Skip to content

New assistant UI#2925

Merged
lethemanh merged 6 commits intomasterfrom
new-assistant-ui
Mar 2, 2026
Merged

New assistant UI#2925
lethemanh merged 6 commits intomasterfrom
new-assistant-ui

Conversation

@lethemanh
Copy link
Copy Markdown
Contributor

@lethemanh lethemanh commented Feb 3, 2026

Result:

Screen.Recording.2026-02-12.at.17.49.01.mov

Related to:

linagora/cozy-home#2351

Feature flags:

  • cozy.source-knowledge.enabled: Enable source knowledge.
  • cozy.search-conversation.enabled: Enable search conversation.
  • cozy.top-bar-in-assistant.enabled: Enable top bar in assistant view.

Summary by CodeRabbit

  • New Features

    • Expanded assistant UI: threaded conversation view, composer, user/assistant message types, conversation lists, sidebar, avatar/selection, search conversations and dedicated assistant view.
    • Real‑time streaming runtime for incremental assistant responses and a knowledge panel with Drive, Mail and Chat sources and selectors.
    • Conversation actions: create, open, share, rename, delete; localized UI strings.
  • Chores

    • Public API extended to expose runtime and adapters; peer React requirement raised.
  • Documentation

    • Added/enriched translations: English, French, Russian, Vietnamese.

@lethemanh lethemanh self-assigned this Feb 3, 2026
@lethemanh lethemanh marked this pull request as draft February 3, 2026 10:14
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR implements a complete assistant feature in packages/cozy-search: adds runtime and UI dependencies, numerous React components (AssistantContainer, AssistantAvatar, Sidebar, ConversationList and items, ConversationComposer, ConversationBar, Conversation, message components, Twake knowledge panel, AssistantView), runtime wiring (CozyAssistantRuntimeProvider, CozyRealtimeChatAdapter, StreamBridge), hooks and helpers (useConversation, buildChatConversationsQuery, formatConversationDate, grouping helper), action factories (share, rename, delete), styles, TypeScript declarations, locale additions, expands AssistantProvider context with new state/setters, and exposes new public exports from the package index.

Possibly related PRs

Suggested reviewers

  • rezk2ll
  • doubleface
  • JF-Cozy
  • zatteo
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'New assistant UI' is vague and generic, using non-specific language that doesn't clarify the actual changes or scope of the work. Use a more specific title that describes the primary change, such as 'Migrate to assistant-ui library' or 'Implement new assistant UI with chat interface'.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch new-assistant-ui

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zatteo
Copy link
Copy Markdown
Member

zatteo commented Feb 4, 2026

To formalize what we said together with @Letheman : we plan to end with a PR with only assistant-ui lib migration:

  • the improved version of the lib migration in this PR (with code cleaned up and correctly organized)
  • no patch if, as we think now, nothing is necessary with React 18
  • without "big new feature" like files etc

@lethemanh lethemanh force-pushed the new-assistant-ui branch 4 times, most recently from 0494154 to f3a5d8b Compare February 9, 2026 04:39
Comment thread packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts Outdated
Comment thread packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts Outdated
Comment thread packages/cozy-search/src/components/adapters/StreamBridge.ts
Comment thread packages/cozy-search/src/components/Assistant/AssistantAvatar.jsx
Comment thread packages/cozy-search/src/components/Search/SearchConversation.jsx Outdated
Comment thread packages/cozy-search/src/components/queries.js Outdated
Comment thread packages/cozy-search/src/components/Conversations/ConversationComposer.jsx Outdated
@lethemanh lethemanh marked this pull request as ready for review February 10, 2026 08:03
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cozy-search/src/components/Assistant/AssistantSelection.jsx (1)

104-104: ⚠️ Potential issue | 🟡 Minor

Hardcoded English string — should use i18n.

"Create Assistant" is a hardcoded English string. Given this PR adds localization support across multiple languages, this should use the translation function for consistency.

🤖 Fix all issues with AI agents
In `@packages/cozy-search/package.json`:
- Line 24: Package.json currently lists the scaffolding CLI "shadcn" as a
regular dependency; move it to devDependencies (or remove it) so it isn't
included in production bundles. Open the packages/cozy-search package.json,
locate the "shadcn" entry and remove it from the top-level "dependencies"
object, then add the same version string under "devDependencies" (or delete it
entirely if scaffolding is complete); ensure package.json remains valid JSON and
run the package manager install to update lockfiles.
- Around line 13-14: Update the package.json peerDependencies so React is
required at version 18+: change the "react" peerDependency from its current
">=16.12.0" (or similar) to ">=18.0.0" in this package's package.json, and make
the same change in any other package.json files that list the same peers (the
entries near the `@assistant-ui/react` and `@assistant-ui/react-markdown`
dependencies). Ensure the peerDependencies object reflects "react": ">=18.0.0"
to match `@assistant-ui/react` v0.12.x peer requirements.

In `@packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts`:
- Around line 70-71: Remove any direct logging of user-authored content in
CozyRealtimeChatAdapter: stop printing the full messages array and the user
query to the console. Replace those console calls in the method that references
the messages variable and the user query with either sanitized/redacted
placeholders (e.g., log message counts, types, or masked text) or remove them
entirely; use non-PII context such as "No user message found" or "Received query
(redacted)" and, if needed for debugging, log only metadata like messages.length
or message IDs. Ensure the console.error/console.log calls in
CozyRealtimeChatAdapter reference only non-PII values and remove any direct
inclusion of the messages or raw query variables.
- Around line 80-86: The two development console.log calls in
CozyRealtimeChatAdapter (the logs around sending the request and after POST)
must be removed or replaced with a gated debug/logger call to avoid leaking user
query data; update the method in CozyRealtimeChatAdapter that sends the request
(the code invoking client.stackClient.fetchJSON with
`/ai/chat/conversations/${conversationId}` and userQuery) to either remove both
console.log lines or route them through the adapter's logger/debug flag (e.g.,
this.logger.debug or a DEBUG environment check) and ensure the userQuery content
is never logged in plaintext.
- Around line 106-115: The StreamBridge map entry for this conversationId is
never removed on normal completion; call cleanup() for the StreamBridge entry
when the for-await loop finishes successfully (i.e. in the branch where
!wasAborted and before/after yielding the final complete event) so the
StreamBridge map doesn't leak. Locate the completion block in
CozyRealtimeChatAdapter where finalText is computed (uses sanitizeChatContent)
and a complete/status stop event is yielded, and invoke cleanup() (the same
function used in error/abort paths) to remove the map entry and call complete()
safely.

In `@packages/cozy-search/src/components/Assistant/AssistantAvatar.jsx`:
- Line 12: The component currently returns undefined when assistant is falsy
(line showing "if (!assistant) return"), which breaks React 16; update the
AssistantAvatar component so that when assistant is not present it returns null
instead of undefined (replace the early return with an explicit null return) to
avoid the "Nothing was returned from render" runtime error.

In `@packages/cozy-search/src/components/Conversations/ConversationBar.jsx`:
- Around line 51-55: The handleKeyDown currently ignores and swallows the
KeyboardEvent — it calls onKeyDown() with no args, causing the parent to be
unable to detect Enter and causing the handler to fire on every keypress; change
handleKeyDown to accept the event (e.g., e or event), early-return if isEmpty,
then call onKeyDown(event) so the parent can inspect event.key (or event.code)
and only act on Enter; update any place that assigns handleKeyDown (e.g., the
input's onKeyDown) to pass the event through as well.
- Around line 40-48: The handler handleSend currently mutates the DOM input
value via inputRef.current.value = '' while the input is controlled by a value
prop, causing a controlled/uncontrolled conflict; remove the direct DOM mutation
and instead let the parent clear the controlled value—either call an existing
prop (onSend) after which the parent should reset value, or introduce/ call a
dedicated callback (e.g., onClear or onValueChange('')) from handleSend so the
parent updates the state; you may keep the height reset
(inputRef.current.style.height = 'auto') but do not set inputRef.current.value
directly in ConversationBar.jsx.

In `@packages/cozy-search/src/components/Conversations/ConversationListItem.jsx`:
- Around line 30-33: The toggleMenu handler currently assumes an event and calls
e.stopPropagation(), which causes a TypeError when ActionsMenu invokes onClose
without an event; update toggleMenu (and the similar handler in
ConversationListItemWider) to accept an optional event and guard calls to
e.stopPropagation()/e.preventDefault() (e.g., if (e && e.stopPropagation) ...)
or split into two handlers: one that toggles state (calls
setIsMenuOpen(!isMenuOpen)) for programmatic/onClose use and a separate onClick
handler that receives the event and calls e.stopPropagation()/e.preventDefault()
before toggling; ensure ActionsMenu's onClose uses the event-safe toggling
handler.

In
`@packages/cozy-search/src/components/Conversations/ConversationListItemWider.jsx`:
- Around line 33-36: The toggleMenu function currently calls e.preventDefault()
unconditionally which will throw if invoked without an event (e.g., from onClose
callbacks); update toggleMenu (the function that toggles isMenuOpen via
setIsMenuOpen) to accept an optional event parameter and guard the
preventDefault call (check that e exists before calling preventDefault, e.g.,
use a conditional or optional chaining) so it safely toggles the menu when
called with or without a DOM event.

In `@packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx`:
- Around line 136-194: The created and updated handlers in the useRealtime call
are identical; extract their common logic into a single shared function (e.g.,
handleConversationChange) that accepts a Conversation, checks res._id ===
conversationId and res.messages, computes newIds, finds lastAssistantMsg,
updates cancelledMessageIdsRef/currentStreamingMessageIdRef as before, and sets
messagesIdRef.current = newIds; then pass that shared function for both the
created and updated keys in the useRealtime payload so created and updated both
call the same handler (keeping references to conversationId, messagesIdRef,
currentStreamingMessageIdRef, and cancelledMessageIdsRef).

In `@packages/cozy-search/src/components/queries.js`:
- Around line 67-79: The query returned by buildChatConversationsQuery is
missing indexFields so Mango may do a full collection scan; update the
definition function for buildChatConversationsQuery (the Q(...) chain for
CHAT_CONVERSATIONS_DOCTYPE) to include
.indexFields(['cozyMetadata.doctypeVersion','cozyMetadata.updatedAt']) (place it
before .sortBy) so the filter and sort can use an index and avoid collection
scans.

In `@packages/cozy-search/src/components/Sidebar/index.jsx`:
- Around line 87-89: The JSX string "Recent chats" is hard-coded; wrap it with
the i18n translator (use t('recent_chats') or existing key style) in the Sidebar
component so it matches other strings (e.g., the nearby t(...) usage), and add
the corresponding "recent_chats" key and translations to all locale files
(en.json, fr.json, ru.json, vi.json). Ensure the key name follows project
conventions and update any imports/usage if necessary so the component uses the
translated value.

In `@packages/cozy-search/src/components/TwakeKnowledges/ChatKnowledge.jsx`:
- Around line 48-54: The ListItem's onClick and the Checkbox's onChange are both
calling onToggleItems, causing double-toggle when clicking the checkbox; remove
the Checkbox onChange handler so only the ListItem onClick triggers
onToggleItems (keep Checkbox checked prop using selectedItems.includes(chat.id)
and leave ListItem's onClick as the single toggle entrypoint referencing
chat.id).

In `@packages/cozy-search/src/components/TwakeKnowledges/DriveKnowledge.jsx`:
- Around line 111-117: The component DriveKnowledge.jsx currently contains
hard-coded placeholder arrays myDriveItems and sharedItems; replace these with
real data by either accepting item lists as props (e.g., add props like
myDriveItems and sharedItems to the component signature and use them instead of
the hard-coded arrays) or implement a data fetch inside the component (call the
backend/API and populate state before rendering), and wire those lists into the
existing props usage (selectedItems, onToggleItems, onClearItems); if this is
temporary scaffolding, add a clear TODO comment next to myDriveItems/sharedItems
indicating they are mocks and must be replaced with real data before production.

In `@packages/cozy-search/src/hooks/useConversation.jsx`:
- Around line 11-20: The goToConversation function currently pushes both
'assistant' and conversationId when the path ends with '/assistant', causing a
duplicate segment; change the branch that handles assistantIndex !== -1 so that
if parts.length > assistantIndex + 1 you replace parts[assistantIndex + 1] with
conversationId, but if parts.length === assistantIndex + 1 you only append the
conversationId (not another 'assistant') — update the logic around
assistantIndex and parts manipulation (in goToConversation) to either set the
next segment or push a single conversationId accordingly.
🟡 Minor comments (14)
packages/cozy-search/src/locales/ru.json-51-51 (1)

51-51: ⚠️ Potential issue | 🟡 Minor

Russian pluralization requires 3 forms, but only 2 are provided.

Russian uses three plural forms (one / few / many). The items_selected key has only two forms. Compare with sources on line 14, which correctly provides three. The current form will produce incorrect text for counts like 1 or 5+.

🐛 Suggested fix
-      "items_selected": "Выбрано %{smart_count} элемента |||| Выбрано %{smart_count} элементов",
+      "items_selected": "Выбран %{smart_count} элемент |||| Выбрано %{smart_count} элемента |||| Выбрано %{smart_count} элементов",
packages/cozy-search/src/actions/rename.jsx-36-36 (1)

36-36: ⚠️ Potential issue | 🟡 Minor

Fix Prettier formatting error — CI is failing.

There's an extra space on this line that Prettier flags. This is blocking the pipeline.

-    action: () => { }
+    action: () => {}
packages/cozy-search/src/components/TwakeKnowledges/DriveKnowledge.jsx-32-37 (1)

32-37: ⚠️ Potential issue | 🟡 Minor

Select-all/clear counts diverge from the rendered list.

The "select all" checkbox on Lines 47-49 considers all items, but the collapsed list only renders customItems (Lines 83-102), which filters out IDs 'my-drive' and 'shared-with-me'. Currently none of the hard-coded items carry those IDs, making the filter dead code. If items with those IDs are ever passed in, the checkbox will toggle items the user can't see.

Either remove the customItems filter and render all items, or make the checkbox logic operate on customItems too.

packages/cozy-search/src/components/Search/SearchConversation.jsx-41-50 (1)

41-50: ⚠️ Potential issue | 🟡 Minor

Fallback to Date.now() silently places conversations without updatedAt into the "today" group.

Line 43 uses Date.now() as fallback when conv.cozyMetadata?.updatedAt is missing. This means any conversation lacking metadata will always appear under "Recent," regardless of when it was actually created. Consider using conv.cozyMetadata?.createdAt as a secondary fallback, or placing such conversations in the "older" group by default.

packages/cozy-search/src/components/Conversations/ConversationListItem.jsx-81-85 (1)

81-85: ⚠️ Potential issue | 🟡 Minor

Missing optional chaining on conversation.cozyMetadata — potential runtime crash.

Line 82 accesses conversation.cozyMetadata.updatedAt without optional chaining. If cozyMetadata is undefined (e.g., for a newly created or malformed conversation), this will throw a TypeError.

Proposed fix
             {formatConversationDate(
-              conversation.cozyMetadata.updatedAt,
+              conversation.cozyMetadata?.updatedAt,
               t,
               lang
             )}
packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgePanel.jsx-134-136 (1)

134-136: ⚠️ Potential issue | 🟡 Minor

Missing alt attribute on <img> — accessibility issue.

Line 135 renders <img src={getIcon()} className="u-mr-1" /> without an alt attribute. This is an accessibility gap and may also trigger lint warnings. Add a descriptive alt or use alt="" if decorative.

Proposed fix
-        <img src={getIcon()} className="u-mr-1" />
+        <img src={getIcon()} className="u-mr-1" alt="" />
packages/cozy-search/src/components/TwakeKnowledges/MailKnowledge.jsx-179-187 (1)

179-187: ⚠️ Potential issue | 🟡 Minor

count={3} is hardcoded and disconnected from the actual item count.

The count prop should derive from the data:

Proposed fix
       <MailSection
         title={t('assistant.twake_knowledges.inbox')}
         icon={EmailIcon}
-        count={3}
+        count={inboxItems.length}
         items={inboxItems}
packages/cozy-search/src/components/TwakeKnowledges/MailKnowledge.jsx-75-75 (1)

75-75: ⚠️ Potential issue | 🟡 Minor

{count && <span>...} will render 0 to the DOM if count is 0.

This is a common React gotcha with the && short-circuit pattern for numbers. Use an explicit boolean check:

Proposed fix
-            {count && <span className={styles['badge']}>{count}</span>}
+            {count > 0 && <span className={styles['badge']}>{count}</span>}
packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgePanel.jsx-179-181 (1)

179-181: ⚠️ Potential issue | 🟡 Minor

Fix formatting to pass CI.

The build pipeline reports extra whitespace on lines 180-181. Apply the formatter to fix:

Proposed fix
             label={
               openedKnowledgePanel === 'drive'
                 ? t('assistant.twake_knowledges.select_folders')
-                : openedKnowledgePanel === 'mail'
-                  ? t('assistant.twake_knowledges.select_emails')
-                  : t('assistant.twake_knowledges.select_messages')
+                : openedKnowledgePanel === 'mail'
+                ? t('assistant.twake_knowledges.select_emails')
+                : t('assistant.twake_knowledges.select_messages')
             }
packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx-74-74 (1)

74-74: ⚠️ Potential issue | 🟡 Minor

CI failure: missing return types on functions.

The build pipeline requires explicit return types on these functions. Add the appropriate return type annotations to satisfy the linter.

  • Line 74: ConversationLoaderJSX.Element | null
  • Line 112: CozyAssistantRuntimeProviderInnerJSX.Element
  • Line 261: cleanup callback — void
  • Line 279: CozyAssistantRuntimeProviderJSX.Element | null

Also applies to: 112-112, 261-261, 279-279

packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgeSelector.jsx-49-51 (1)

49-51: ⚠️ Potential issue | 🟡 Minor

Missing alt attribute on chip icon images.

The <img> elements lack alt text, which hinders accessibility. Add an empty alt="" if decorative, or a descriptive label.

Proposed fix
-               <img src={twakeKnowledge.icon} className="u-h-1 u-ml-half" />
+               <img src={twakeKnowledge.icon} alt="" className="u-h-1 u-ml-half" />
packages/cozy-search/src/components/Conversations/ConversationListItemWider.jsx-89-91 (1)

89-91: ⚠️ Potential issue | 🟡 Minor

Potential crash if conversation.cozyMetadata is undefined.

If a conversation document lacks cozyMetadata, accessing .updatedAt will throw. Add optional chaining.

Proposed fix
-       {formatConversationDate(conversation.cozyMetadata.updatedAt, t, lang)}
+       {formatConversationDate(conversation.cozyMetadata?.updatedAt, t, lang)}
packages/cozy-search/src/actions/share.jsx-36-36 (1)

36-36: ⚠️ Potential issue | 🟡 Minor

Fix formatting: remove extra whitespace.

The linter flags a formatting violation here. Also, the action is a no-op — if the share feature isn't implemented yet, consider adding a TODO comment to clarify intent.

Proposed fix
-    action: () => { }
+    // TODO: implement share action
+    action: () => {}
packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgeSelector.jsx-70-72 (1)

70-72: ⚠️ Potential issue | 🟡 Minor

Margin applied to the wrong last item when a chip is hidden by feature flag.

index is relative to the filtered array, but twakeKnowledges.length is the unfiltered array length. When the chat chip is hidden, the last visible chip still receives u-mr-half, causing a trailing gap.

Proposed fix

Store the filtered array first and use its length:

+ const filteredKnowledges = twakeKnowledges.filter(k => k.display)
+
  return (
    <div className="u-flex u-flex-row u-flex-wrap u-flex-items-center u-flex-justify-end">
-     {twakeKnowledges
-       .filter(twakeKnowledge => twakeKnowledge.display)
-       .map((twakeKnowledge, index) => {
+     {filteredKnowledges.map((twakeKnowledge, index) => {
          ...
              className={cx('u-mr-0', styles['knowledge-chips-item'], {
-               'u-mr-half': index < twakeKnowledges.length - 1
+               'u-mr-half': index < filteredKnowledges.length - 1
              })}
🧹 Nitpick comments (27)
packages/cozy-search/src/types.d.ts (2)

24-28: useEventListener handler signature drops the event parameter.

The handler is typed as () => void, but event listener callbacks typically receive an Event argument. Consumers passing (e) => { e.preventDefault(); ... } would see a type mismatch.

Proposed fix
 declare module 'cozy-ui/transpiled/react/hooks/useEventListener' {
   // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-  const useEventListener: (element: any, event: string, handler: () => void) => void
+  const useEventListener: (element: any, event: string, handler: (event: Event) => void) => void
   export default useEventListener
 }

50-56: useI18n return type may be missing polyglot.

The actual useI18n from twake-i18n also returns polyglot (used internally by useExtendI18n). If any consumer in this package ever needs it, the type would be incomplete. Fine for now if only t and lang are used.

packages/cozy-search/src/components/helpers.js (1)

66-75: Hardcoded hour12: true overrides locale preference.

Passing lang to toLocaleTimeString is good, but hour12: true forces 12-hour (AM/PM) format for all locales, including those that conventionally use 24-hour time (e.g., fr, ru, vi). Consider removing hour12 to let the locale decide, or use hourCycle only when explicitly needed.

♻️ Suggested fix
     const timeStr = date.toLocaleTimeString(lang, {
       hour: 'numeric',
-      minute: '2-digit',
-      hour12: true
+      minute: '2-digit'
     })
packages/cozy-search/src/components/AssistantProvider.jsx (1)

133-168: Context value grows large — consider splitting in the future.

The provider now exposes 18+ values. Every state change re-creates the context object and re-renders all consumers. This isn't a regression (it follows the existing pattern), but as the surface keeps growing, consider splitting into separate contexts (e.g., a TwakeKnowledgeContext) to limit unnecessary re-renders.

packages/cozy-search/src/components/Messages/UserMessage.jsx (1)

19-23: Inline component re-created on every render.

The Text component passed to MessagePrimitive.Parts is an anonymous arrow function, so it's recreated each render. This can cause unnecessary unmount/remount cycles for the rendered elements. Extract it to a stable reference.

♻️ Suggested fix
+const TextPart = ({ text }) => <Typography>{text}</Typography>
+
 const UserMessage = () => {
   return (
     <MessagePrimitive.Root className="u-mt-1">
       <Card
         className={cx(
           'u-bg-paleGrey u-bdrs-5 u-bdw-0 u-ml-auto u-p-half',
           styles['cozyThread-user-messages']
         )}
       >
         <MessagePrimitive.Parts
           components={{
-            Text: ({ text }) => <Typography>{text}</Typography>
+            Text: TextPart
           }}
         />
       </Card>
     </MessagePrimitive.Root>
   )
 }
packages/cozy-search/src/components/adapters/StreamBridge.ts (1)

75-95: Missing return() method on the async iterator.

When a for await...of loop exits early (via break, throw, or return), the runtime calls iterator.return(). Without it, the stream won't self-clean. Currently mitigated because the adapter calls cleanup() explicitly, but adding return() would make the iterator protocol-complete and safer for other consumers.

Suggested addition
       [Symbol.asyncIterator]() {
         return this
-      }
+      },
+      return(): Promise<IteratorResult<string>> {
+        isDone = true
+        return Promise.resolve({ value: undefined as unknown as string, done: true })
+      }
     }
packages/cozy-search/package.json (1)

17-18: Both classnames and clsx are listed — they serve the same purpose.

These two libraries are functionally equivalent for conditional class name joining. Consider consolidating on one (preferably clsx since it's smaller) to avoid redundancy.

packages/cozy-search/src/components/styles.styl (1)

19-21: Consider using 100dvh instead of 100vh for mobile viewport accuracy.

100vh on mobile browsers includes the URL bar area, which can cause content to be clipped or scrollbars to appear unexpectedly. 100dvh (dynamic viewport height) adjusts for the actual visible area. If this view targets mobile as well, consider:

 .assistantWrapper
-  height calc(100vh - 48px)
+  height calc(100dvh - 48px)

If only desktop is targeted, 100vh is fine.

packages/cozy-search/src/components/Assistant/AssistantSelectionItem.jsx (1)

12-12: Simplify the import path — this file is already in the Assistant/ directory.

Since AssistantSelectionItem.jsx is located in components/Assistant/, the import ../Assistant/AssistantAvatar unnecessarily traverses up and back into the same directory. Use a direct sibling import instead.

Proposed fix
-import AssistantAvatar from '../Assistant/AssistantAvatar'
+import AssistantAvatar from './AssistantAvatar'
packages/cozy-search/src/components/TwakeKnowledges/styles.styl (2)

30-35: WebKit-only scrollbar styling won't apply in Firefox.

The ::-webkit-scrollbar rules are ignored by Firefox and other non-Chromium browsers. If consistent scrollbar styling matters, consider adding scrollbar-width: thin and scrollbar-color for Firefox.

Proposed addition for Firefox support
 .source-panel-content
   flex 1
   overflow-y auto
   padding 0 8px
+  scrollbar-width thin
+  scrollbar-color var(--dividerColor) transparent

   &::-webkit-scrollbar
     width 6px

46-51: Excessive !important usage across utility classes.

Many rules here use !important on nearly every property (e.g., .section-header, .nested-item, .clear-all-button). This is likely to override cozy-ui/Material UI defaults, but it makes future maintenance harder. Consider using more specific selectors or scoping via a parent class instead, where feasible.

packages/cozy-search/src/components/Assistant/AssistantContainer.jsx (1)

27-35: Hard-coded u-bg-white may break dark mode / theming.

Both the sidebar wrapper (Line 27) and the main content area (Line 33) use u-bg-white, while the stylesheets elsewhere (e.g., styles.styl) use var(--paperBackgroundColor) for theme compatibility. Consider using a theme-aware background utility or CSS variable instead.

packages/cozy-search/src/index.ts (1)

7-12: Public API additions look reasonable.

One note: CozyComposer (Line 10) re-exports ConversationComposer — the naming mismatch between the public alias and the internal module name may cause confusion when navigating between consumer code and source. Consider aligning the names.

packages/cozy-search/src/components/Sidebar/index.jsx (1)

28-32: No loading or error state for the conversations query.

When conversations is undefined (loading) or the query fails, the sidebar shows nothing. Consider showing a spinner or skeleton while loading, and handling the error case for better UX.

packages/cozy-search/src/components/TwakeKnowledges/ChatKnowledge.jsx (1)

17-22: Hardcoded mock data should be replaced with real data or clearly marked as placeholder.

The chats array is hardcoded with static entries ('Team Discussion', 'Project Updates', etc.). Unlike the conversation queries used elsewhere (e.g., buildChatConversationsQuery in SearchConversation.jsx), this component doesn't fetch real data. If this is intentional scaffolding for a future integration, consider adding a // TODO comment. If it should already be dynamic, wire it to a query or accept it as a prop.

Additionally, since this array is static, move it outside the component to avoid re-creating it on every render.

Proposed minimal fix
+const CHATS = [
+  { id: 'chat1', name: 'Team Discussion' },
+  { id: 'chat2', name: 'Project Updates' },
+  { id: 'chat3', name: 'General Chat' },
+  { id: 'chat4', name: 'Support' }
+]
+
 const ChatKnowledge = ({ selectedItems, onToggleItems, onClearItems }) => {
   const { t } = useI18n()
-  const chats = [
-    { id: 'chat1', name: 'Team Discussion' },
-    { id: 'chat2', name: 'Project Updates' },
-    { id: 'chat3', name: 'General Chat' },
-    { id: 'chat4', name: 'Support' }
-  ]
+  // TODO: Replace with real data from API
+  const chats = CHATS
packages/cozy-search/src/components/Search/SearchConversation.jsx (2)

58-62: SearchBar is not wired to any search/filter logic.

The SearchBar renders but has no onChange, onSearch, or value binding. It's currently non-functional. If this is intentional scaffolding, a // TODO comment would help. Otherwise, it should be wired to filter conversations before grouping.


20-24: buildChatConversationsQuery() is called on every render without memoization.

Each call returns a fresh object. While cozy-client's useQuery likely deduplicates by the as key, it's a good practice to stabilize the reference (e.g., call it outside the component or wrap in useMemo).

Proposed fix
 const SearchConversation = () => {
   const { t } = useI18n()
   const { createNewConversation, goToConversation } = useConversation()

-  const conversationsQuery = buildChatConversationsQuery()
+  const conversationsQuery = useMemo(() => buildChatConversationsQuery(), [])
   const { data: conversations } = useQuery(
     conversationsQuery.definition,
     conversationsQuery.options
   )
packages/cozy-search/src/components/Conversations/ConversationListItem.jsx (1)

35-35: makeActions is called on every render.

makeActions([share, rename, remove], { t }) creates a new actions array each render. Consider memoizing it with useMemo:

Proposed fix
-  const actions = makeActions([share, rename, remove], { t })
+  const actions = useMemo(() => makeActions([share, rename, remove], { t }), [t])
packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgePanel.jsx (1)

90-127: Three parallel switch statements on openedKnowledgePanel can be consolidated into a config map.

getTitle, getDescription, and getIcon all switch on the same key. A single lookup object would be more maintainable and eliminate the repetition:

Proposed refactor
+const PANEL_CONFIG = {
+  drive: { titleKey: 'title_drive', descKey: 'desc_drive', icon: TDrive },
+  mail: { titleKey: 'title_mail', descKey: 'desc_mail', icon: TMail },
+  chat: { titleKey: 'title_chat', descKey: 'desc_chat', icon: TChat }
+}

 // Inside the component:
-  const getTitle = () => { ... }
-  const getDescription = () => { ... }
-  const getIcon = () => { ... }
+  const config = PANEL_CONFIG[openedKnowledgePanel] || {}
+  const title = config.titleKey ? t(`assistant.twake_knowledges.${config.titleKey}`) : t('assistant.twake_knowledges.title_default')
+  const description = config.descKey ? t(`assistant.twake_knowledges.${config.descKey}`) : ''
+  const icon = config.icon || null
packages/cozy-search/src/components/Views/AssistantDialog.jsx (2)

62-62: Redundant ternary — both branches are identical.

title={isMobile ? ' ' : ' '} always evaluates to ' '. Simplify to title=" ".

Proposed fix
-      title={isMobile ? ' ' : ' '}
+      title=" "

53-60: Inline style on dialogContent — consider using a CSS class.

The rest of the component uses utility classes and .styl modules for styling. The inline style object on dialogContent (lines 54-59) is inconsistent with that pattern and will create a new object reference on every render.

packages/cozy-search/src/components/TwakeKnowledges/MailKnowledge.jsx (1)

143-165: Hardcoded mock data — same concern as ChatKnowledge.jsx.

inboxItems and starredItems are hardcoded with placeholder email data. If this is intentional scaffolding, add // TODO comments to indicate these should be replaced with real data fetches. Also, these arrays are re-created on every render — move them outside the component if they remain static for now.

Also applies to: 167-175

packages/cozy-search/src/components/Assistant/AssistantAvatar.jsx (1)

14-52: Consider extracting the repeated className computation.

The same cx(styles['assistant-icon'], { [styles['assistant-icon--small']]: isSmall }, className) expression is duplicated across all three branches. Extracting it into a variable would reduce repetition and make future changes less error-prone.

Proposed refactor
 const AssistantAvatar = ({ assistant, isSmall, className }) => {
   if (!assistant) return null
 
+  const iconClassName = cx(
+    styles['assistant-icon'],
+    { [styles['assistant-icon--small']]: isSmall },
+    className
+  )
+
   if (assistant.id !== DEFAULT_ASSISTANT.id && !assistant.icon) {
     return (
       <Icon
         icon={AssistantIcon}
-        className={cx(
-          styles['assistant-icon'],
-          {
-            [styles['assistant-icon--small']]: isSmall
-          },
-          className
-        )}
+        className={iconClassName}
       />
     )
   }

Apply the same replacement for the other two branches.

packages/cozy-search/src/components/Conversations/ConversationListItemWider.jsx (1)

38-38: makeActions is called on every render.

This creates new action instances and a new array reference on each render, which can trigger unnecessary re-renders of ActionsMenu. Consider memoizing with useMemo.

Proposed fix
- const actions = makeActions([share, rename, remove], { t })
+ const actions = useMemo(() => makeActions([share, rename, remove], { t }), [t])

Add useMemo to the React import on line 2.

packages/cozy-search/src/components/Conversations/Conversation.jsx (1)

16-19: Unnecessary spread destructure.

{ ...queryMyselfResult } creates a shallow copy for no apparent benefit. This looks like a leftover from when data was extracted. Simplify to a direct assignment.

Proposed fix
- const { ...queryMyselfResult } = useQuery(
+ const queryMyselfResult = useQuery(
    myselfQuery.definition,
    myselfQuery.options
  )
packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx (2)

114-114: new StreamBridge() runs on every render.

useRef(new StreamBridge()) evaluates the constructor on each render, discarding the result after the first. Use lazy initialization to avoid wasteful allocations.

Proposed fix
- const streamBridgeRef = useRef(new StreamBridge())
+ const streamBridgeRef = useRef<StreamBridge | null>(null)
+ if (!streamBridgeRef.current) {
+   streamBridgeRef.current = new StreamBridge()
+ }

122-124: useEffect with empty deps references initialMessages.

The effect reads initialMessages but has [] as deps, so it only runs on mount. This is intentionally "run once," but it will trigger an ESLint react-hooks/exhaustive-deps warning. Suppress it explicitly to signal intent.

Proposed fix
  useEffect(() => {
    messagesIdRef.current = initialMessages.map(m => m.id).filter((id): id is string => !!id)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

Comment thread packages/cozy-search/package.json Outdated
Comment thread packages/cozy-search/package.json Outdated
Comment thread packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts Outdated
Comment thread packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts Outdated
Comment thread packages/cozy-search/src/components/queries.js
Comment thread packages/cozy-search/src/components/Sidebar/index.jsx Outdated
Comment thread packages/cozy-search/src/hooks/useConversation.jsx
Comment thread packages/cozy-search/package.json Outdated
)
}

export default AssistantViewWithProviders
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand, how is the top bar displayed? It is the same top bar than the app and the full screen modal has a margin top?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The top bar is placed in cozy-home and cannot be displayed in the assistant if we use modal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we still display the assistant 100% full screen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean display assistant without top bar, right? If so, we still can display like that with the flag cozy.top-bar-in-assistant.enabled is false

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/cozy-search/package.json (1)

52-53: ⚠️ Potential issue | 🟠 Major

Dev React version (16.12.0) is incompatible with @assistant-ui/react.

The devDependencies pin React at 16.12.0, but @assistant-ui/react requires ^18 || ^19. This means tests and local development using these assistant-ui components will fail or produce misleading results. Bump the dev React/ReactDOM versions to at least 18.x to match the runtime requirements.

Proposed fix
-    "react": "16.12.0",
-    "react-dom": "16.13.0",
+    "react": "18.2.0",
+    "react-dom": "18.2.0",
packages/cozy-search/src/components/Assistant/AssistantSelection.jsx (1)

104-104: ⚠️ Potential issue | 🟡 Minor

Hardcoded English string "Create Assistant" should use the translation function.

This string will not be localized for fr/ru/vi users. Use useI18n or the t function to get a translated label.

Proposed fix (assuming a translation key exists or will be added)
-              <Typography variant="body1">Create Assistant</Typography>
+              <Typography variant="body1">{t('assistant.create')}</Typography>
🤖 Fix all issues with AI agents
In `@packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts`:
- Around line 120-126: The code in CozyRealtimeChatAdapter currently yields the
raw backend/network error message (errorMessage) into the chat UI; change this
so the yielded content uses a generic user-facing string like "An unexpected
error occurred. Please try again later." (do not include error.message or error
details), and separately log the real error for debugging (e.g., call
logger.error or console.error with the Error object) before yielding; update the
errorMessage handling and the yield block that returns content: [{ type: 'text',
text: `Error: ${errorMessage}` }] and status: { type: 'incomplete', reason:
'error' } to use the generic message while preserving the existing status shape.

In `@packages/cozy-search/src/components/Conversations/Conversation.jsx`:
- Around line 15-24: The code currently runs buildMyselfQuery() and
useQuery(...) (queryMyselfResult) but never uses the returned data; either
remove the unused query or document that it's intentional prefetching. To fix:
if you don't need the data, delete buildMyselfQuery() and the useQuery(...)
invocation (and the isLoading check), removing queryMyselfResult and
isQueryLoading usage; otherwise add a short clarifying comment above the
buildMyselfQuery/useQuery block stating it is intentional prefetching to warm
the cozy-client cache and keep isLoading handling as-is. Reference symbols:
buildMyselfQuery, useQuery, queryMyselfResult, isQueryLoading,
ConversationComposer, UserMessage, AssistantMessage.

In
`@packages/cozy-search/src/components/Conversations/ConversationListItemWider.jsx`:
- Around line 55-59: The code accesses conversation.messages[...] directly and
can crash if messages is undefined or empty; update the accesses in
ConversationListItemWider.jsx (the JSX that renders primary and the secondary
message preview) to safely handle missing or short message arrays by deriving a
safeMessages = conversation.messages || [] (or using optional chaining and a
length check) and then indexing only after verifying safeMessages.length >= 2
(or >=1 for the last item), falling back to an empty string or placeholder when
no message exists; apply this same guard to both places that reference
conversation.messages[...] (the primary block using the second-to-last message
and the other usage around lines 85–87).

In `@packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgePanel.jsx`:
- Around line 30-32: The useEffect that calls setSelectedItems based on
selectedTwakeKnowledge[openedKnowledgePanel] causes local selectedItems to be
overwritten whenever selectedTwakeKnowledge reference changes (even for
unrelated panels), leading to stale-state flickers; to fix, change the
synchronization to only initialize or sync when the openedKnowledgePanel
actually changes or when the incoming value truly differs (compare by panel key
or deep equality) OR switch to a key-based remount so the component is
re-mounted per panel (e.g., use key={openedKnowledgePanel} on this panel
component), and ensure useEffect/selectors reference useEffect,
setSelectedItems, selectedTwakeKnowledge, openedKnowledgePanel, and
selectedItems so local toggles/clears are not clobbered by upstream object
identity changes.
- Around line 149-154: The SearchBar in TwakeKnowledgePanel.jsx is rendered
without state or handlers; add a controlled input and filtering: inside the
TwakeKnowledgePanel component create local state (e.g., const [query, setQuery]
= useState('')), pass value={query} and onChange={e => setQuery(e.target.value)}
to the SearchBar, and apply a filter (e.g., filter by name or content) to the
array of knowledges/entries before mapping/rendering them so the UI updates as
the user types; if this is intentionally WIP, instead add a clear TODO comment
above the SearchBar explaining the missing wiring.
- Line 135: The img rendered by TwakeKnowledgePanel (the element using
getIcon()) is missing an alt attribute; update the JSX in
TwakeKnowledgePanel.jsx to include a meaningful alt string (or alt="" if the
icon is purely decorative) on the <img> tag so screen readers get appropriate
information; if the alt text should reflect the icon type, derive it from the
same source that selects the icon (e.g., pass a label or key alongside getIcon()
or compute an alt based on the knowledge item) and set that value as the img's
alt prop.

In `@packages/cozy-search/src/locales/ru.json`:
- Line 51: The "items_selected" translation in ru.json currently has only two
plural forms; update the "items_selected" value to include three Russian plural
forms separated by "||||" (singular, paucal, plural) mirroring the pattern used
for the "sources" key: provide an appropriate singular form for smart_count=1,
the 2-4 form, and the 0/5+ form so Russian declension is correct (edit the
"items_selected" entry to include all three variants).
🧹 Nitpick comments (15)
packages/cozy-search/src/components/Assistant/AssistantSelectionItem.jsx (1)

12-12: Unnecessary path traversal — file is already in Assistant/.

Since AssistantSelectionItem.jsx lives in the Assistant directory, ../Assistant/AssistantAvatar goes up a level only to come back into the same folder. A direct relative import is cleaner.

Suggested fix
-import AssistantAvatar from '../Assistant/AssistantAvatar'
+import AssistantAvatar from './AssistantAvatar'
packages/cozy-search/src/components/TwakeKnowledges/styles.styl (2)

43-64: Excessive !important usage suggests specificity conflicts.

Nearly every property in .section-header, .nested-item, and .clear-all-button uses !important. This typically indicates fighting against a UI library's inline styles or high-specificity selectors. While it works, it makes future style overrides very difficult and is fragile to upstream changes.

Consider whether these classes can be applied with higher specificity selectors (e.g., .source-panel .section-header) or by using the UI library's built-in styling/theming mechanisms (e.g., sx prop, classes override) instead of !important.


30-35: WebKit-only scrollbar styling.

The ::-webkit-scrollbar pseudo-elements only work in Chromium/WebKit browsers. Firefox users will see the default scrollbar. If cross-browser custom scrollbar styling matters, consider adding scrollbar-width: thin and scrollbar-color for Firefox support.

Proposed addition for Firefox support
 .source-panel-content
   flex 1
   overflow-y auto
   padding 0 8px
+  scrollbar-width thin
+  scrollbar-color var(--dividerColor) transparent

   &::-webkit-scrollbar
     width 6px
packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgePanel.jsx (1)

90-127: Three parallel switch statements on the same key can be consolidated into a config map.

getTitle, getDescription, and getIcon all switch on openedKnowledgePanel with identical cases. A single lookup object would reduce duplication and make adding new panels trivial.

Suggested refactor
+const PANEL_CONFIG = {
+  drive: { titleKey: 'title_drive', descKey: 'desc_drive', icon: TDrive },
+  mail:  { titleKey: 'title_mail',  descKey: 'desc_mail',  icon: TMail },
+  chat:  { titleKey: 'title_chat',  descKey: 'desc_chat',  icon: TChat }
+}
+
 const TwakeKnowledgePanel = ({ onClose }) => {
   const { t } = useI18n()
   // ...
+  const panelConfig = PANEL_CONFIG[openedKnowledgePanel] || {}
+  const title = t(`assistant.twake_knowledges.${panelConfig.titleKey || 'title_default'}`)
+  const description = panelConfig.descKey ? t(`assistant.twake_knowledges.${panelConfig.descKey}`) : ''
+  const icon = panelConfig.icon || null

Then use title, description, and icon directly in the JSX, removing the three getX functions.

packages/cozy-search/package.json (1)

17-18: Remove clsx from direct dependencies—it's pulled in transitively by class-variance-authority.

classnames is actively used throughout the codebase (15+ files), while clsx has zero direct imports. Since class-variance-authority already declares clsx as a dependency, there's no reason to list it as a direct dependency.

packages/cozy-search/src/components/Conversations/Conversation.jsx (1)

16-16: Unnecessary spread destructure.

const { ...queryMyselfResult } is equivalent to const queryMyselfResult = useQuery(...). The spread-into-rest adds no value here.

♻️ Simplify
-  const { ...queryMyselfResult } = useQuery(
+  const queryMyselfResult = useQuery(
     myselfQuery.definition,
     myselfQuery.options
   )
packages/cozy-search/src/components/Conversations/ConversationBar.jsx (1)

77-100: IconButton wrapping Button is redundant nesting.

Both IconButton and Button are interactive controls. While component="div" on the inner Button avoids an illegal nested <button>, the IconButton wrapper serves no visible purpose — it adds an extra DOM node and click target that doesn't contribute styling or semantics beyond what the Button already provides. Consider using just one of the two.

Simplify by removing the IconButton wrapper
-            endAdornment: isRunning ? (
-              <IconButton className="u-p-0 u-mr-half">
-                <Button
-                  size="small"
-                  component="div"
-                  className="u-miw-auto u-w-2 u-h-2 u-bdrs-circle"
-                  classes={{ label: 'u-flex u-w-auto' }}
-                  label={<Icon icon={StopIcon} size={12} />}
-                  onClick={onCancel}
-                />
-              </IconButton>
-            ) : (
-              <IconButton className="u-p-0 u-mr-half">
-                <Button
-                  size="small"
-                  component="div"
-                  className="u-miw-auto u-w-2 u-h-2 u-bdrs-circle"
-                  classes={{ label: 'u-flex u-w-auto' }}
-                  label={<Icon icon={PaperplaneIcon} size={12} rotate={-45} />}
-                  disabled={isEmpty}
-                  onClick={handleSend}
-                />
-              </IconButton>
+            endAdornment: isRunning ? (
+              <Button
+                size="small"
+                className="u-miw-auto u-w-2 u-h-2 u-bdrs-circle u-mr-half"
+                classes={{ label: 'u-flex u-w-auto' }}
+                label={<Icon icon={StopIcon} size={12} />}
+                onClick={onCancel}
+              />
+            ) : (
+              <Button
+                size="small"
+                className="u-miw-auto u-w-2 u-h-2 u-bdrs-circle u-mr-half"
+                classes={{ label: 'u-flex u-w-auto' }}
+                label={<Icon icon={PaperplaneIcon} size={12} rotate={-45} />}
+                disabled={isEmpty}
+                onClick={handleSend}
+              />
             ),
packages/cozy-search/src/actions/delete.jsx (1)

9-24: Duplicated makeComponent across delete, share, and rename action files.

The makeComponent function is nearly identical in delete.jsx, share.jsx, and rename.jsx. The only variations are the className prop and displayName. Consider extracting a shared factory that accepts { displayName, className } to reduce duplication.

♻️ Shared factory sketch (e.g., in a new `makeActionComponent.jsx`)
import React, { forwardRef } from 'react'
import ActionsMenuItem from 'cozy-ui/transpiled/react/ActionsMenu/ActionsMenuItem'
import Icon from 'cozy-ui/transpiled/react/Icon'
import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
import ListItemText from 'cozy-ui/transpiled/react/ListItemText'

export const makeActionComponent = (label, icon, { displayName, className } = {}) => {
  const Component = forwardRef((props, ref) => (
    <ActionsMenuItem className={className} {...props} ref={ref}>
      <ListItemIcon>
        <Icon className={className} icon={icon} />
      </ListItemIcon>
      <ListItemText primary={label} />
    </ActionsMenuItem>
  ))
  Component.displayName = displayName || 'ActionComponent'
  return Component
}
packages/cozy-search/src/components/adapters/StreamBridge.ts (2)

75-95: Consider adding a return() method on the async iterator.

The iterator lacks a return() method, which means if a consumer uses break in a for await loop or the loop is otherwise interrupted, the iterator's internal cleanup won't run automatically per the async iterator protocol. Currently, the adapter handles this externally via streamBridge.cleanup(), so it works in practice. However, implementing return() would make the iterator self-contained and safer for any future consumers.

♻️ Proposed addition
       iterator: {
         next: (): Promise<IteratorResult<string>> =>
           new Promise((resolve, reject) => {
             // ... existing logic
           }),
+        return: (): Promise<IteratorResult<string>> => {
+          if (!isDone) {
+            isDone = true
+            if (resolveNext) {
+              resolveNext({ value: undefined as unknown as string, done: true })
+              resolveNext = null
+              rejectNext = null
+            }
+          }
+          return Promise.resolve({ value: undefined as unknown as string, done: true })
+        },
         [Symbol.asyncIterator]() {
           return this
         }
       }

16-18: cleanupCallback is a single global callback, not per-conversation.

If StreamBridge is ever used to manage multiple simultaneous conversations, the single cleanupCallback will fire for any conversation's cleanup. Currently the provider uses one conversation at a time (route-driven), so this is fine — just noting for future awareness.

packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx (1)

122-124: Missing dependency in useEffect will trigger exhaustive-deps lint warning.

initialMessages is used inside the effect but not listed in the dependency array. While the intent (run on mount only) is valid since this component is remounted per conversation, the empty [] will trigger a React lint warning. Suppress it explicitly to signal intent.

♻️ Proposed fix
   useEffect(() => {
     messagesIdRef.current = initialMessages.map(m => m.id).filter((id): id is string => !!id)
-  }, [])
+  // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally run only on mount; initialMessages is stable for this component's lifetime
+  }, [])
packages/cozy-search/src/components/styles.styl (1)

20-21: Magic number 48px — consider using a CSS variable or a comment.

The 48px presumably corresponds to a header/toolbar height. A brief comment or a shared CSS variable (e.g., --header-height) would improve maintainability and make the coupling explicit.

Also note that 100vh on mobile browsers doesn't account for the dynamic address bar. If mobile support matters, 100dvh (dynamic viewport height) is more reliable, though it requires checking your browser support targets.

packages/cozy-search/src/components/Sidebar/index.jsx (1)

28-32: buildChatConversationsQuery() is re-invoked on every render.

Consider hoisting the query outside the component or wrapping in useMemo to avoid rebuilding the query descriptor object each render cycle. It's functionally harmless (useQuery caches by options.as), but it's a cheap win for clarity.

Proposed fix
+const conversationsQuery = buildChatConversationsQuery()
+
 const Sidebar = () => {
   ...
-  const conversationsQuery = buildChatConversationsQuery()
   const { data: conversations } = useQuery(
     conversationsQuery.definition,
     conversationsQuery.options
   )
packages/cozy-search/src/components/Views/AssistantDialog.jsx (1)

62-62: Redundant ternary — both branches are identical.

isMobile ? ' ' : ' ' evaluates to ' ' in all cases. If the title should differ between mobile and desktop, update accordingly; otherwise simplify.

Proposed fix
-      title={isMobile ? ' ' : ' '}
+      title=" "
packages/cozy-search/src/components/Conversations/ConversationListItemWider.jsx (1)

38-38: makeActions is recomputed on every render.

Consider memoizing with useMemo since the inputs (t) only change on locale switch.

Proposed fix
-  const actions = makeActions([share, rename, remove], { t })
+  const actions = useMemo(() => makeActions([share, rename, remove], { t }), [t])

Add useMemo to the React import on Line 2.

Comment thread packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts Outdated
Comment thread packages/cozy-search/src/components/Conversations/Conversation.jsx Outdated
Comment thread packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgePanel.jsx Outdated
Comment thread packages/cozy-search/src/locales/ru.json
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cozy-search/src/components/Assistant/AssistantSelection.jsx (1)

98-106: ⚠️ Potential issue | 🟡 Minor

Hardcoded English string "Create Assistant" — should use t() for i18n.

All other user-facing strings in this PR go through useI18n, but this label is a raw English string. It will not be translated.

Proposed fix

You'd need to import useI18n and add a locale key, then:

-            <Typography variant="body1">Create Assistant</Typography>
+            <Typography variant="body1">{t('assistant.sidebar.create_new')}</Typography>

(or a dedicated key if create_new doesn't match the intended label)

🤖 Fix all issues with AI agents
In `@packages/cozy-search/src/components/Conversations/ConversationBar.jsx`:
- Around line 76-99: The JSX nests an IconButton (outer) around a Button
(inner), which produces invalid nested <button> elements and causes
inaccessible/incorrect click behavior; update ConversationBar.jsx by removing
the IconButton wrapper and apply any necessary classes/props directly to the
inner Button (the components referenced are IconButton and Button), ensuring the
active handlers (onClick: handleSend and onCancel) and props (disabled tied to
isEmpty, size, component, className, classes, label/icons, rotate) are preserved
on the single rendered interactive element when isRunning toggles; alternatively
replace IconButton with a non-interactive wrapper (span/div) if additional
layout is required so clicks on the full area trigger the inner handler.
- Around line 40-48: The current handleSend function calls both onSend() and
onAssistantExecute(), causing duplicate POSTs (the
composerRuntime/CozyRealtimeChatAdapter already posts when onSend triggers).
Remove the redundant onAssistantExecute() invocation from handleSend (keep
onSend() and the inputRef height reset) so only onSend() performs the backend
request; update any related comments if needed to note that
CozyRealtimeChatAdapter.run now handles the POST.

In
`@packages/cozy-search/src/components/Conversations/ConversationListItemWider.jsx`:
- Around line 60-69: The dots IconButton is inside the ListItem whose onClick
calls onOpenConversation(conversation._id), so clicking the IconButton triggers
both toggleMenu and the ListItem navigation; update the toggleMenu handler used
by the IconButton (reference: toggleMenu function and IconButton with ref
anchorRef) to call event.stopPropagation() (and event.preventDefault() if
appropriate) at the start so the click on IconButton does not bubble to the
ListItem onClick that calls onOpenConversation(conversation._id).

In `@packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx`:
- Around line 202-212: The useMemo that creates adapter via
createCozyRealtimeChatAdapter is missing t in its dependency array, causing
stale translations when locale changes; update the useMemo dependency list for
adapter (the call that references client, conversationId,
streamBridgeRef.current and t) to include t so the adapter is recreated on
locale/translation updates.
- Around line 125-147: handleConversationChange is recreated each render causing
realtime subscriptions to hold stale callbacks; wrap it with React.useCallback
and add conversationId (and any other used refs/values if needed) to its
dependency array so the callback identity only changes when its dependencies
change, then use the memoized handleConversationChange in the useRealtime
subscription setup to ensure subscribe/unsubscribe use matching callback
references (refer to handleConversationChange, currentStreamingMessageIdRef,
messagesIdRef, cancelledMessageIdsRef).

In `@packages/cozy-search/src/components/helpers.js`:
- Around line 66-75: The time formatting forces 12-hour output by hardcoding
hour12: true in the date.toLocaleTimeString call; remove the hour12 property so
toLocaleTimeString(date, lang, {...}) uses the locale passed in (lang) and
formats times correctly for locales like fr, ru, vi—update the block that
computes timeStr (where isToday/isYesterday are used) to call
date.toLocaleTimeString(lang, { hour: 'numeric', minute: '2-digit' }) instead of
including hour12.

In `@packages/cozy-search/src/components/TwakeKnowledges/MailKnowledge.jsx`:
- Line 75: In MailKnowledge.jsx the badge check "{count && <span
className={styles['badge']}>{count}</span>}" will render "0" when count is 0;
change the condition to explicitly guard against null/undefined and zero (e.g.,
use "count != null && count !== 0 && <span
className={styles['badge']}>{count}</span>" or "typeof count === 'number' &&
count > 0 && <span className={styles['badge']}>{count}</span>") so the span is
only rendered for a positive numeric count.

In `@packages/cozy-search/src/components/Views/AssistantDialog.jsx`:
- Line 62: The title prop in AssistantDialog.jsx uses a dead ternary
(title={isMobile ? ' ' : ' '}) which always yields a single space; update the
JSX to either remove the ternary and pass the intended static value directly
(e.g., title=" " or title="") or make the branches meaningful (e.g.,
title={isMobile ? '' : ' '}) depending on desired mobile vs desktop behavior;
look for the title prop usage in the AssistantDialog component and adjust the
expression accordingly to eliminate the redundant ternary.

In `@packages/cozy-search/src/locales/fr.json`:
- Around line 37-45: The French locale is missing the assistant.time and
assistant.message translation blocks, causing untranslated keys in
Conversation.jsx (t('assistant.message.welcome')) and AssistantMessage.jsx
(t('assistant.message.running')); add an "assistant" object to
packages/cozy-search/src/locales/fr.json with a "time" block (today/yesterday)
and a "message" block (welcome/running) immediately after the existing
"search_conversation" block (before "twake_knowledges"), using French
translations for those keys so the components render localized text.

In `@packages/cozy-search/src/locales/vi.json`:
- Around line 43-49: Add the missing assistant.message translation block to the
Vietnamese locale by defining the keys assistant.message.welcome and
assistant.message.running with appropriate Vietnamese strings (matching the
English intent), e.g., add an "assistant": { "message": { "welcome": "...",
"running": "..." } } object at the top level of the vi.json so those keys exist
and mirror the English locale's entries.
🧹 Nitpick comments (19)
packages/cozy-search/src/components/Messages/UserMessage.jsx (1)

19-23: Extract the inline Text component to a stable reference.

The anonymous arrow function passed as Text creates a new component identity on every render, which can cause unnecessary unmount/remount cycles. Define it outside UserMessage.

♻️ Proposed fix
+const Text = ({ text }) => <Typography>{text}</Typography>
+
 const UserMessage = () => {
   return (
     <MessagePrimitive.Root className="u-mt-1">
       <Card
         className={cx(
           'u-bg-paleGrey u-bdrs-5 u-bdw-0 u-ml-auto u-p-half',
           styles['cozyThread-user-messages']
         )}
       >
         <MessagePrimitive.Parts
           components={{
-            Text: ({ text }) => <Typography>{text}</Typography>
+            Text
           }}
         />
       </Card>
     </MessagePrimitive.Root>
   )
 }
packages/cozy-search/src/types.d.ts (1)

24-28: handler type is too restrictive — it should accept an event parameter.

The handler is typed as () => void, but event listener callbacks typically receive an Event object. This will cause type errors for any caller that needs to read the event.

Proposed fix
 declare module 'cozy-ui/transpiled/react/hooks/useEventListener' {
-  const useEventListener: (element: any, event: string, handler: () => void) => void
+  const useEventListener: (element: any, event: string, handler: (event: Event) => void) => void
   export default useEventListener
 }
packages/cozy-search/src/components/helpers.js (1)

49-51: No validation for invalid date strings.

new Date('garbage') produces an Invalid Date object, and subsequent comparisons and toLocaleTimeString/toLocaleDateString calls will return "Invalid Date" strings rendered in the UI. A quick guard would prevent this.

Proposed fix
 export const formatConversationDate = (dateString, t, lang) => {
   if (!dateString) return ''
   const date = new Date(dateString)
+  if (isNaN(date.getTime())) return ''
   const now = new Date()
packages/cozy-search/src/components/TwakeKnowledges/styles.styl (1)

1-93: Stylesheet looks reasonable for the panel layout.

The Stylelint error on line 2 is a false positive — Stylus syntax doesn't use colons for property-value pairs.

The heavy !important usage throughout (lines 46–47, 49–51, 54–60, 84–85, 88, 91–93) suggests overriding component library defaults. This works but can become fragile if upstream styles change. Consider scoping via more specific selectors where feasible.

packages/cozy-search/src/components/TwakeKnowledges/MailKnowledge.jsx (1)

24-139: MailSection and DriveSection are nearly identical — consider extracting a shared KnowledgeSection component.

Both components have the same collapsible header pattern, tri-state checkbox logic, clear-all button, and nested item list. The only differences are the item rendering (mail shows subject/from/preview/date; drive shows folder name). A shared wrapper accepting a custom renderItem prop would eliminate the duplication.

Also applies to: 18-107

packages/cozy-search/src/components/TwakeKnowledges/DriveKnowledge.jsx (1)

35-37: customItems filter is dead code — no items currently have IDs 'my-drive' or 'shared-with-me'.

The hardcoded items use IDs like 'doc1', 'proj1', etc., so this filter returns the same array as items. If this is forward-looking logic for real data, add a comment to clarify. Otherwise, it adds confusion.

packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgePanel.jsx (1)

59-127: Four switch statements on openedKnowledgePanel — consolidate into a config map.

renderContent, getTitle, getDescription, and getIcon all switch on the same key. A single config object would eliminate the repetition and make adding new panel types a one-line change.

Suggested approach
+const PANEL_CONFIG = {
+  drive: {
+    component: DriveKnowledge,
+    titleKey: 'assistant.twake_knowledges.title_drive',
+    descKey: 'assistant.twake_knowledges.desc_drive',
+    icon: TDrive
+  },
+  mail: {
+    component: MailKnowledge,
+    titleKey: 'assistant.twake_knowledges.title_mail',
+    descKey: 'assistant.twake_knowledges.desc_mail',
+    icon: TMail
+  },
+  chat: {
+    component: ChatKnowledge,
+    titleKey: 'assistant.twake_knowledges.title_chat',
+    descKey: 'assistant.twake_knowledges.desc_chat',
+    icon: TChat
+  }
+}

Then use PANEL_CONFIG[openedKnowledgePanel] to look up all four values at once.

packages/cozy-search/src/components/Conversations/ConversationComposer.jsx (2)

26-28: handleSend has no empty-input guard — relies on ConversationBar for protection.

composerRuntime.send() is called unconditionally here. The guard (if (isEmpty) return) lives inside ConversationBar.handleSend (for button clicks) and ConversationBar.handleKeyDown (for Enter). This works for the current wiring, but if handleSend is ever called from a different path, an empty message could be dispatched. Consider adding a guard here as well, or document the assumption.

💡 Optional: add a local guard
  const handleSend = useCallback(() => {
+   if (isEmpty) return
    composerRuntime.send()
- }, [composerRuntime])
+ }, [composerRuntime, isEmpty])

61-66: Layout with single child when feature flag is off.

When cozy.create-assistant.enabled is false, only TwakeKnowledgeSelector renders inside the u-flex-justify-between container. A single child with justify-between behaves like justify-start, pushing the knowledge selector to the left. If the intent is to keep it right-aligned regardless, u-flex-justify-end would be more robust.

packages/cozy-search/src/components/Assistant/styles.styl (1)

23-36: Heavy use of !important for menu-item overrides.

Five !important declarations to override the base ActionsMenuItem styles suggests a specificity battle. This works but is fragile — any future cozy-ui update that adds its own !important will break these overrides. If possible, consider increasing specificity via nesting or a more specific selector instead.

packages/cozy-search/src/actions/share.jsx (2)

9-24: makeComponent is duplicated across share.jsx, rename.jsx, and delete.jsx.

The makeComponent helper in share.jsx and rename.jsx is identical. Even delete.jsx only differs by adding className="u-error". Consider extracting a shared makeActionComponent(label, icon, options?) utility to reduce duplication.


36-36: Share action is a no-op.

action: () => { } means clicking "Share" does nothing. If this is intentionally stubbed out for a future PR, consider adding a brief comment or a // TODO so it's not mistaken for a bug.

packages/cozy-search/src/actions/rename.jsx (1)

9-24: makeComponent is duplicated across action files.

This helper is identical in rename.jsx, delete.jsx, and share.jsx (per AI summary). Consider extracting it into a shared utility to reduce duplication. Low priority given the small footprint.

packages/cozy-search/src/components/Views/AssistantDialog.jsx (1)

53-60: Inline style object is re-created on every render.

The dialogContent.style object is a new reference each render, which can defeat shallow-comparison optimizations downstream. Consider extracting it as a module-level constant.

Proposed fix
+const dialogContentStyle = {
+  display: 'flex',
+  flexDirection: 'column',
+  flexGrow: 1,
+  padding: 0
+}
+
 const AssistantDialog = () => {
   ...
        dialogContent: {
-          style: {
-            display: 'flex',
-            flexDirection: 'column',
-            flexGrow: 1,
-            padding: 0
-          }
+          style: dialogContentStyle
        }
packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx (2)

112-114: useEffect intentionally omits initialMessages from deps — add suppression comment.

This effect seeds messagesIdRef once on mount. The missing dependency is intentional, but a future contributor (or linter) may "fix" it and introduce a bug. An eslint-disable-next-line comment clarifies intent.

  useEffect(() => {
    messagesIdRef.current = initialMessages.map(m => m.id).filter((id): id is string => !!id)
+    // eslint-disable-next-line react-hooks/exhaustive-deps -- One-time initialization from initial load
  }, [])

66-95: ConversationLoader renders null during loading — consider a loading indicator.

Returning null while the conversation query loads means the user sees a blank screen. A spinner or skeleton would improve perceived responsiveness, especially on slow connections.

packages/cozy-search/src/components/Conversations/ConversationListItemWider.jsx (1)

38-38: actions array is recreated on every render.

makeActions(...) is called unconditionally on each render. Consider memoizing with useMemo keyed on t.

- const actions = makeActions([share, rename, remove], { t })
+ const actions = useMemo(() => makeActions([share, rename, remove], { t }), [t])
packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts (1)

87-104: Abort is only checked between chunks, not proactively.

If the stream is blocked waiting for the next chunk (no data arriving), abortSignal.aborted won't be evaluated until the next for await iteration completes. For a responsive cancel, consider listening to abortSignal.addEventListener('abort', ...) to call streamBridge.cleanup() immediately. This is a minor UX concern — the stop button may feel unresponsive if the backend stalls.

packages/cozy-search/src/components/adapters/StreamBridge.ts (1)

24-26: cleanupCallback is a singleton shared across all conversations.

setCleanupCallback stores a single callback, not per-conversation. If StreamBridge were ever used for multiple concurrent conversations (e.g., prefetching), cleanup for one conversation would invoke the other's callback. Currently safe given the 1:1 provider-to-conversation relationship, but worth noting for future changes.

Comment thread packages/cozy-search/src/components/Conversations/ConversationListItemWider.jsx Outdated
Comment thread packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx Outdated
Comment thread packages/cozy-search/src/components/helpers.js
Comment thread packages/cozy-search/src/components/TwakeKnowledges/MailKnowledge.jsx Outdated
Comment thread packages/cozy-search/src/components/Views/AssistantDialog.jsx Outdated
Comment thread packages/cozy-search/src/locales/fr.json
Comment thread packages/cozy-search/src/locales/vi.json
@lethemanh lethemanh force-pushed the new-assistant-ui branch 3 times, most recently from 22a7fcc to 1d44629 Compare February 11, 2026 07:25
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/cozy-search/package.json (2)

51-52: ⚠️ Potential issue | 🟠 Major

devDependencies still pin React 16 — tests will run against an unsupported version.

peerDependencies now correctly require >=18.0.0, but the dev versions used for testing are still react@16.12.0 / react-dom@16.13.0. This means:

  1. Unit tests execute against a React version the library no longer supports.
  2. @assistant-ui/react internals (e.g., useSyncExternalStore, concurrent features) will break or silently degrade under React 16, making test results unreliable.

Bump both to 18.x (e.g., 18.2.0) to match the declared peer range.

-    "react": "16.12.0",
-    "react-dom": "16.13.0",
+    "react": "18.2.0",
+    "react-dom": "18.2.0",

32-32: ⚠️ Potential issue | 🟠 Major

Upgrade @testing-library/react to v13+ for React 18 compatibility.

@testing-library/react v10.4.9 uses the legacy ReactDOM.render API, which is incompatible with React 18. Version 13 and newer require and support React 18 with createRoot-based rendering.

Suggested change
-    "@testing-library/react": "10.4.9",
+    "@testing-library/react": "14.2.1",
🤖 Fix all issues with AI agents
In `@packages/cozy-search/package.json`:
- Around line 13-14: Update the package.json dependency entry for
`@assistant-ui/react-markdown` to a 0.12.x range to match `@assistant-ui/react`
v0.12.x (change the version spec for "@assistant-ui/react-markdown" from
"^0.11.0" to "^0.12.0" or a compatible 0.12.x); ensure the updated version
aligns with the peer dependency requirement for `@assistant-ui/react` (e.g.,
^0.12.9) so both "@assistant-ui/react" and "@assistant-ui/react-markdown" are on
compatible 0.12.x releases.

In `@packages/cozy-search/src/components/adapters/StreamBridge.ts`:
- Around line 74-94: The iterator.next implementation allows multiple concurrent
pending calls because resolveNext/rejectNext are overwritten; update
iterator.next in StreamBridge to guard against concurrent calls by detecting
when resolveNext or rejectNext is already set and immediately reject the new
Promise (or return the existing pending promise) instead of overwriting;
reference the iterator.next method and the resolveNext/rejectNext, queue,
isDone, push/complete/error flow so you add the guard at the start of next() to
prevent orphaning earlier promises and ensure only one outstanding consumer
promise is tracked.

In `@packages/cozy-search/src/components/Conversations/ConversationListItem.jsx`:
- Around line 79-86: The code accesses conversation.cozyMetadata.updatedAt
directly in ConversationListItem.jsx which can throw if cozyMetadata is
undefined; update the call to formatConversationDate to use optional chaining
(e.g., pass conversation.cozyMetadata?.updatedAt) and ensure
formatConversationDate can accept undefined/nullable timestamps (or provide a
sensible fallback) so the Typography render is safe when cozyMetadata is
missing.

In `@packages/cozy-search/src/components/Conversations/styles.styl`:
- Around line 79-85: The action button (.conversation-list-item-action) is
currently hidden with display: none !important and only shown on :hover, making
it inaccessible to keyboard users; add a :focus-within rule on
.conversation-list-item to reveal .conversation-list-item-action when the list
item or its children receive keyboard focus (mirror the hover behavior) and
ensure the rule has at least the same specificity/importance as the existing
display: none !important so keyboard-only users can tab to and operate the dots
menu.

In `@packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx`:
- Around line 18-21: The project imports AssistantRuntimeProvider and
useLocalRuntime from `@assistant-ui/react` which requires React ^18/^19 but the
package currently uses React 16.12.0; fix by upgrading the cozy-search package
React peer (and any react-dom/test-utils) to ^18 (or ^19) in package.json and
run a reinstall and test, or if upgrading is not possible remove/replace
`@assistant-ui/react` usage (e.g., eliminate
AssistantRuntimeProvider/useLocalRuntime and their consumers) with a compatible
library; update any React APIs used in CozyAssistantRuntimeProvider.tsx to React
18-compatible patterns (keep references to AssistantRuntimeProvider and
useLocalRuntime when deciding whether to upgrade or remove).

In `@packages/cozy-search/src/components/helpers.js`:
- Around line 49-82: The function formatConversationDate does not guard against
unparseable dateString values; after const date = new Date(dateString) add a
validity check (e.g., if (isNaN(date.getTime())) return '' or a localized
fallback) to short-circuit before computing isToday/isYesterday or calling
toLocaleDateString; this prevents returning "Invalid Date" and keeps behavior
consistent with the existing empty-string guard for falsy inputs.

In `@packages/cozy-search/src/components/Messages/MarkdownText.jsx`:
- Around line 1-14: The component MarkdownText uses the React 18+ hook
useMessagePartText from `@assistant-ui/react` which is incompatible with the
project's React 16.12.0; fix by either upgrading React to ^18 (update project
peer dependency and rebuild) or remove the offending hook and implement a
React-16-compatible replacement: modify MarkdownText to obtain the message text
from an alternative source (e.g., a prop passed into MarkdownText or a local
context/provider) instead of calling useMessagePartText; ensure references to
useMessagePartText are removed and that MarkdownText still renders Markdown with
the text via the existing Markdown component.

In `@packages/cozy-search/src/components/Search/SearchConversation.jsx`:
- Around line 42-44: The current logic in SearchConversation.jsx sets date = new
Date(conv.cozyMetadata?.updatedAt || Date.now()).getTime(), which forces items
missing updatedAt into the "today" bucket; change the fallback to prefer
conv.cozyMetadata?.createdAt before Date.now() (e.g. use
conv.cozyMetadata?.updatedAt ?? conv.cozyMetadata?.createdAt) or alternatively
skip/flag conversations missing both timestamps so they aren't grouped as
"today"; update the code that computes the date value (the date variable) to
apply this new fallback/filtering.
- Around line 58-62: The SearchBar is rendered but not wired to state or
filtering logic; update SearchConversation.jsx to manage a search query state
(e.g., useState for query) and pass it into the SearchBar via value and onChange
(or onInput) so user input updates the query, then use that query to filter the
conversations list (or invoke a provided callback prop like onSearchChange)
before rendering results; if this is intentionally a WIP, add a clear TODO
comment next to the SearchBar noting that value/onChange and filtering must be
implemented.
- Around line 20-24: buildChatConversationsQuery() currently gets called on
every render which returns a new object and causes useQuery (consuming
conversationsQuery.definition/options) to re-run; to fix, move the call out of
the render or memoize it: replace the inline call to
buildChatConversationsQuery() with a stable value (e.g., wrap the call in
useMemo inside SearchConversation component or export a module-level constant)
so conversationsQuery (and its .definition/.options) keep stable references
across renders and prevent unnecessary re-fetches.

In `@packages/cozy-search/src/types.d.ts`:
- Around line 24-28: The handler type for useEventListener is too
narrow—currently declared as () => void which rejects handlers that accept an
Event parameter; update the declaration of useEventListener so the handler
accepts an event (e.g., (event: Event) => void or (event?: Event) => void to
allow both signatures) and, while here, consider tightening the element type
from any to EventTarget | null to better reflect usage; edit the
useEventListener declaration accordingly.
🧹 Nitpick comments (19)
packages/cozy-search/src/types.d.ts (1)

50-56: useI18n return type is incomplete — missing polyglot.

The actual useI18n hook (see packages/twake-i18n/src/useExtendI18n.jsx) also returns polyglot in its return value, which is used by useExtendI18n. While current consumers in this PR only destructure t and lang, omitting polyglot from the type makes the declaration inaccurate and could cause type errors for future callers.

Proposed fix
 declare module 'twake-i18n' {
   export function useI18n(): {
     t: (key: string, options?: Record<string, unknown>) => string
     lang: string
+    polyglot: any
   }
   export function useExtendI18n(locales: Record<string, unknown>): void
 }
packages/cozy-search/src/components/styles.styl (1)

20-21: Consider documenting or extracting the magic 48px value.

The 48px offset presumably corresponds to a header/toolbar height. A comment clarifying what it accounts for would help future maintainers.

packages/cozy-search/src/components/Messages/UserMessage.jsx (1)

19-23: Extract the inline Text component to avoid re-creation on every render.

The inline ({ text }) => <Typography>{text}</Typography> creates a new component reference each render, which can cause MessagePrimitive.Parts to unmount/remount its children unnecessarily.

♻️ Proposed fix

Define the component outside UserMessage:

+const TextPart = ({ text }) => <Typography>{text}</Typography>
+
 const UserMessage = () => {
   return (
     <MessagePrimitive.Root className="u-mt-1">
       <Card
         className={cx(
           'u-bg-paleGrey u-bdrs-5 u-bdw-0 u-ml-auto u-p-half',
           styles['cozyThread-user-messages']
         )}
       >
         <MessagePrimitive.Parts
           components={{
-            Text: ({ text }) => <Typography>{text}</Typography>
+            Text: TextPart
           }}
         />
       </Card>
     </MessagePrimitive.Root>
   )
 }
packages/cozy-search/src/components/Messages/AssistantMessage.jsx (1)

15-15: Stabilize the useMessage selector to avoid potential extra re-renders.

If useMessage uses reference equality on the selector (common pattern), a new arrow function each render could cause unnecessary re-subscriptions.

♻️ Proposed fix
+const selectIsRunning = s => s.status?.type === 'running'
+
 const AssistantMessage = () => {
   const { t } = useI18n()
-  const isRunning = useMessage(s => s.status?.type === 'running')
+  const isRunning = useMessage(selectIsRunning)
packages/cozy-search/src/components/TwakeKnowledges/styles.styl (2)

43-93: Excessive !important usage and generic class names risk style collisions.

There are ~15 !important declarations, suggesting these styles are fighting the component library's specificity. Additionally, generic names like .badge, .section-header, .nested-item could easily clash with other styles in the app.

Consider:

  1. Namespacing classes (e.g., .twakeKnowledge-badge, .twakeKnowledge-sectionHeader) to avoid collisions.
  2. Investigating whether higher specificity selectors or component-level style overrides could replace !important.

30-35: WebKit-only scrollbar styling — Firefox users get default scrollbar.

The custom scrollbar is only styled with -webkit-scrollbar pseudo-elements. Consider adding scrollbar-width: thin and scrollbar-color for Firefox support.

♻️ Proposed fix
 .source-panel-content
   flex 1
   overflow-y auto
   padding 0 8px
+  scrollbar-width thin
+  scrollbar-color var(--dividerColor) transparent

   &::-webkit-scrollbar
     width 6px
packages/cozy-search/src/components/Assistant/styles.styl (1)

23-36: Consider reducing !important usage.

There are six !important annotations across .menu-item and .menu-item-icon-button. While sometimes necessary to override library (cozy-ui) defaults, heavy reliance on !important makes future style overrides difficult. Consider increasing specificity or using cozy-ui's own class overrides where possible.

packages/cozy-search/src/components/AssistantProvider.jsx (1)

133-168: Context is growing large — consider splitting concerns.

The AssistantContext now exposes 15+ values. Every state change (e.g., toggling isOpenSearchConversation) triggers re-renders for all consumers, even those that only use selectedAssistantId. Consider splitting into separate contexts (e.g., AssistantUIContext for UI state, AssistantKnowledgeContext for knowledge panel state) if performance becomes an issue.

packages/cozy-search/src/components/TwakeKnowledges/MailKnowledge.jsx (1)

143-175: Hard-coded data: add a TODO comment for replacement with real data.

Similar to DriveKnowledge.jsx, inboxItems and starredItems are temporary scaffolding. Add a // TODO: replace with real data source comment to track this, and plan for data fetching, error handling, and loading states.

Based on learnings: "In packages/cozy-search/src/components/TwakeKnowledges/DriveKnowledge.jsx, the hard-coded myDriveItems and sharedItems arrays are temporary scaffolding and will be replaced with real data later."

packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx (1)

112-114: Suppress the exhaustive-deps lint warning for this intentional mount-only effect.

initialMessages is intentionally captured only at mount time, but ESLint's react-hooks/exhaustive-deps rule will flag it. Add a suppression comment to document the intent.

Proposed fix
   useEffect(() => {
     messagesIdRef.current = initialMessages.map(m => m.id).filter((id): id is string => !!id)
+  // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally capture initial value only
   }, [])
packages/cozy-search/src/components/Views/AssistantDialog.jsx (1)

53-60: Consider extracting inline styles to a CSS class.

The inline style object on dialogContent will create a new object reference on every render. Since you already import styles.styl, moving these declarations into a stylesheet class would be more consistent with the rest of the component.

packages/cozy-search/src/components/adapters/StreamBridge.ts (2)

74-94: Missing return() method on the async iterator.

The AsyncIterableIterator protocol optionally supports return() for early termination (e.g., break from for-await-of). Without it, breaking out of a consuming loop won't signal the bridge to clean up the stream.

Proposed addition
         [Symbol.asyncIterator]() {
           return this
-        }
+        },
+        return(): Promise<IteratorResult<string>> {
+          isDone = true
+          return Promise.resolve({ value: undefined as unknown as string, done: true })
+        }

63-72: Queued chunks are discarded on error.

When error() is called, isDone is set to true and the error is stored. Since next() checks error before draining the queue (line 77), any buffered chunks are lost. If partial delivery matters, consider draining the queue before rejecting. If discarding is intentional, a brief comment would clarify the design choice.

packages/cozy-search/src/actions/share.jsx (2)

9-24: makeComponent is duplicated across share.jsx, rename.jsx, and delete.jsx.

All three action files define nearly identical makeComponent factories (differing only in displayName and optional class names like u-error for delete). Consider extracting a shared helper, e.g. makeActionComponent(displayName, className), to reduce duplication.


36-36: action is a no-op — add a TODO comment.

The share action handler does nothing. If this is intentional WIP, a // TODO: implement share action comment would signal intent to future maintainers.

packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgePanel.jsx (1)

90-127: Consolidate repeated switch statements into a config map.

getTitle, getDescription, and getIcon all switch on the same openedKnowledgePanel value. A single lookup object would reduce repetition and make adding new panel types easier.

Example refactor
+const PANEL_CONFIG = {
+  drive: { icon: TDrive, titleKey: 'title_drive', descKey: 'desc_drive' },
+  mail: { icon: TMail, titleKey: 'title_mail', descKey: 'desc_mail' },
+  chat: { icon: TChat, titleKey: 'title_chat', descKey: 'desc_chat' }
+}
+
 const TwakeKnowledgePanel = ({ onClose }) => {
   // ...
+  const config = PANEL_CONFIG[openedKnowledgePanel]
+  const title = config ? t(`assistant.twake_knowledges.${config.titleKey}`) : t('assistant.twake_knowledges.title_default')
+  const description = config ? t(`assistant.twake_knowledges.${config.descKey}`) : ''
+  const icon = config?.icon ?? null
packages/cozy-search/src/components/Conversations/ConversationListItem.jsx (1)

35-35: makeActions is called on every render.

This creates new action objects each render cycle, which could cause unnecessary re-renders of ActionsMenu. Consider memoizing with useMemo.

Proposed fix
- const actions = makeActions([share, rename, remove], { t })
+ const actions = useMemo(() => makeActions([share, rename, remove], { t }), [t])

Add useMemo to the React import:

-import React, { useState, useRef } from 'react'
+import React, { useState, useRef, useMemo } from 'react'
packages/cozy-search/src/components/Conversations/Conversation.jsx (1)

38-43: Nit: object shorthand for component props.

UserMessage: UserMessage and AssistantMessage: AssistantMessage can use ES6 shorthand.

Proposed fix
           <ThreadPrimitive.Messages
             components={{
-              UserMessage: UserMessage,
-              AssistantMessage: AssistantMessage
+              UserMessage,
+              AssistantMessage
             }}
           />
packages/cozy-search/package.json (1)

17-18: classnames and clsx serve the same purpose — consider consolidating.

Both libraries do conditional class-name joining. clsx was likely pulled in alongside the shadcn/assistant-ui patterns while classnames was already in use. Keeping both adds unnecessary bundle weight and creates inconsistency in which utility developers reach for.

Pick one (typically clsx since it's smaller and already required by class-variance-authority) and migrate the classnames call sites.

Comment thread packages/cozy-search/package.json Outdated
Comment thread packages/cozy-search/src/components/adapters/StreamBridge.ts
Comment thread packages/cozy-search/src/components/Conversations/ConversationListItem.jsx Outdated
Comment thread packages/cozy-search/src/components/Conversations/styles.styl
Comment thread packages/cozy-search/src/components/CozyAssistantRuntimeProvider.tsx Outdated
Comment thread packages/cozy-search/src/components/Messages/MarkdownText.jsx
Comment thread packages/cozy-search/src/components/Search/SearchConversation.jsx Outdated
Comment thread packages/cozy-search/src/components/Search/SearchConversation.jsx Outdated
Comment on lines +58 to +62
<SearchBar
className="u-mb-2"
placeholder={t('assistant.search_conversation.placeholder')}
size="medium"
/>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

SearchBar is rendered but not wired to any filtering logic.

The search bar has no value, onChange, or connected filter. Users will type into it with no effect. If this is intentional WIP, add a TODO comment; otherwise wire it up.

🤖 Prompt for AI Agents
In `@packages/cozy-search/src/components/Search/SearchConversation.jsx` around
lines 58 - 62, The SearchBar is rendered but not wired to state or filtering
logic; update SearchConversation.jsx to manage a search query state (e.g.,
useState for query) and pass it into the SearchBar via value and onChange (or
onInput) so user input updates the query, then use that query to filter the
conversations list (or invoke a provided callback prop like onSearchChange)
before rendering results; if this is intentionally a WIP, add a clear TODO
comment next to the SearchBar noting that value/onChange and filtering must be
implemented.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temporary skip the functions for search bar

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lethemanh, understood! I've noted that the SearchBar functionality is temporarily deferred for now.


✏️ Learnings added
Learnt from: lethemanh
Repo: linagora/cozy-libs PR: 2925
File: packages/cozy-search/src/components/Search/SearchConversation.jsx:49-55
Timestamp: 2026-02-12T03:37:51.940Z
Learning: In `packages/cozy-search/src/components/Search/SearchConversation.jsx`, the SearchBar component (around lines 58-62) is intentionally rendered without value/onChange/filtering logic as a temporary placeholder. The search functionality is planned to be implemented later.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: lethemanh
Repo: linagora/cozy-libs PR: 2911
File: packages/cozy-search/src/components/Views/CreateAssistantDialog.jsx:52-67
Timestamp: 2026-01-24T17:24:53.584Z
Learning: In `packages/cozy-search/src/components/Views/CreateAssistantDialog.jsx` and `EditAssistantDialog.jsx`, the dialog closing after submission is handled by the `useAssistantDialog` hook's `handleNext` function, which calls `onClose()` after awaiting the `onSubmit` callback passed to it. The local `onSubmit` functions in these dialogs do not need to call `onClose()` directly.

Comment thread packages/cozy-search/src/types.d.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cozy-search/package.json (1)

51-52: ⚠️ Potential issue | 🟠 Major

Dev React versions (16.x) conflict with peer dependency requirement (≥18.0.0).

Lines 84–85 require react >=18.0.0 and react-dom >=18.0.0, but the dev dependencies pin React 16.12.0 and React-DOM 16.13.0. This means tests and local development run against a React version that the library explicitly declares as unsupported. Several new dependencies (@assistant-ui/react, zustand v5) also require React 18+, so tests will likely fail or behave incorrectly.

Upgrade both to at least 18.0.0:

Proposed fix
-    "react": "16.12.0",
-    "react-dom": "16.13.0",
+    "react": "18.0.0",
+    "react-dom": "18.0.0",

You may also need to upgrade @testing-library/react (v10 → v13+) and drop @testing-library/react-hooks (merged into @testing-library/react ≥13.1).

🤖 Fix all issues with AI agents
In `@packages/cozy-search/package.json`:
- Around line 84-85: The peerDependency change in package.json raises "react"
and "react-dom" to ">=18.0.0" which is a breaking change for consumers on React
16/17; update the package version from "0.16.2" to a new semver-major (or
appropriate 0.x increment per your release policy) and add a migration note in
the release changelog/README explaining the React 18 requirement and migration
steps; ensure the version field in package.json (currently "0.16.2") is bumped
and that the changelog/release notes mention the peer dependency change and any
required consumer migration steps.

In `@packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts`:
- Around line 118-121: The rendered message currently prepends a hardcoded
English "Error: " before the translated string in the yielded content; update
the yield in CozyRealtimeChatAdapter (the block that returns content: [{ type:
'text', text: ... }]) to remove the hardcoded prefix or move it into your i18n
keys so the whole message is translated (e.g., replace the template string with
a single t(...) call for a fully localized key like 'assistant.error' or use a
new key that includes the prefix). Ensure the change targets the yield that
builds the error content object and only uses t(...) for the full message.

In `@packages/cozy-search/src/components/Search/helpers.js`:
- Line 8: The early return `if (!conversations) return {}` returns a different
shape than the normal path and causes callers expecting `{ today: [], older: []
}` to crash; change the early return in the helper to return `{ today: [],
older: [] }` instead so functions like `groups.today.map(...)` are safe, and
ensure the function that processes `conversations` (the same helper)
consistently returns that shape for all code paths.
🧹 Nitpick comments (18)
packages/cozy-search/src/types.d.ts (1)

50-56: useI18n declaration omits additional properties returned by the hook.

From packages/twake-i18n/src/useExtendI18n.jsx, the real hook also returns polyglot (and potentially other fields). The current declaration is fine if only t and lang are used in this package, but consider adding an index signature or a comment noting the partial typing so future consumers aren't surprised.

packages/cozy-search/src/components/Search/helpers.js (1)

23-23: Date.now() fallback silently bins conversations with missing updatedAt into "today".

If a conversation has no cozyMetadata.updatedAt, the fallback to Date.now() will always place it in the "today" group regardless of when it was actually created. If this is intentional, a brief comment would help future readers; otherwise, consider falling back to conv.cozyMetadata?.createdAt or placing such items in "older" as a safer default.

packages/cozy-search/package.json (1)

17-18: Redundant class-name utilities: both classnames and clsx are listed.

These libraries serve the same purpose (conditional class string joining). clsx is smaller and is the one expected by the shadcn/tailwind-merge pattern (cn helper typically wraps clsx + twMerge). Consider migrating existing classnames usage to clsx and dropping the classnames dependency to avoid shipping two libraries that do the same thing.

packages/cozy-search/src/components/TwakeKnowledges/styles.styl (2)

30-35: WebKit-only scrollbar styling — no effect on Firefox.

::-webkit-scrollbar rules are not supported by Firefox. If consistent scrollbar appearance matters, consider adding scrollbar-width: thin and scrollbar-color for Firefox support.

Proposed addition
 .source-panel-content
   flex 1
   overflow-y auto
   padding 0 8px
+  scrollbar-width thin
+  scrollbar-color var(--dividerColor) transparent

   &::-webkit-scrollbar
     width 6px

46-51: Heavy !important usage throughout the stylesheet.

Many rules use !important (lines 46-47, 50-51, 54-60, 63-64, 84-85, 88, 91-93). This is common when overriding cozy-ui/MUI defaults, but it makes future style adjustments harder. Consider whether some of these can be replaced with more specific selectors to reduce !important reliance over time.

packages/cozy-search/src/actions/rename.jsx (2)

9-24: makeComponent is duplicated across rename, share, and delete action files.

The makeComponent helper is nearly identical in rename.jsx, share.jsx, and delete.jsx (the only variation being delete.jsx adding className="u-error"). Consider extracting it to a shared utility to reduce duplication.


36-36: No-op action handler — consider adding a TODO.

The action callback is an empty function. If this is intentional WIP, a // TODO: implement rename action comment would help other developers understand the incomplete state.

packages/cozy-search/src/components/TwakeKnowledges/TwakeKnowledgePanel.jsx (1)

59-127: Four parallel switch statements on the same key could be a config map.

renderContent, getTitle, getDescription, and getIcon all switch on openedKnowledgePanel. A config object would reduce repetition and make adding new panel types a one-line change.

Example consolidation
+const PANEL_CONFIG = {
+  drive: {
+    titleKey: 'assistant.twake_knowledges.title_drive',
+    descKey: 'assistant.twake_knowledges.desc_drive',
+    icon: TDrive,
+    Component: DriveKnowledge
+  },
+  mail: {
+    titleKey: 'assistant.twake_knowledges.title_mail',
+    descKey: 'assistant.twake_knowledges.desc_mail',
+    icon: TMail,
+    Component: MailKnowledge
+  },
+  chat: {
+    titleKey: 'assistant.twake_knowledges.title_chat',
+    descKey: 'assistant.twake_knowledges.desc_chat',
+    icon: TChat,
+    Component: ChatKnowledge
+  }
+}

Then inside the component:

const config = PANEL_CONFIG[openedKnowledgePanel]
// use config.titleKey, config.icon, config.Component, etc.
packages/cozy-search/src/components/Views/AssistantDialog.jsx (1)

53-60: Consider extracting the inline style to a constant.

A new object is created on every render. Since the values are static, hoist them outside the component or use useMemo.

♻️ Suggested refactor
+const dialogContentStyle = {
+  display: 'flex',
+  flexDirection: 'column',
+  flexGrow: 1,
+  padding: 0
+}
+
 const AssistantDialog = () => {
   ...
       dialogContent: {
-          style: {
-            display: 'flex',
-            flexDirection: 'column',
-            flexGrow: 1,
-            padding: 0
-          }
+          style: dialogContentStyle
        }
packages/cozy-search/src/components/adapters/StreamBridge.ts (2)

74-94: Missing return() method on the async iterator.

When a for-await-of loop is broken (e.g., via break, throw, or return), the engine calls iterator.return() to signal cleanup. Without it, the stream remains open until the adapter explicitly calls cleanup(). Adding return() makes the iterator self-cleaning and more robust if consumed outside the adapter.

♻️ Suggested addition
       iterator: {
         next: (): Promise<IteratorResult<string>> =>
           new Promise((resolve, reject) => {
             // ... existing logic
           }),
+        return: (): Promise<IteratorResult<string>> => {
+          if (!isDone) {
+            isDone = true
+            if (resolveNext) {
+              resolveNext({ value: undefined as unknown as string, done: true })
+              resolveNext = null
+              rejectNext = null
+            }
+          }
+          return Promise.resolve({ value: undefined as unknown as string, done: true })
+        },
         [Symbol.asyncIterator]() {
           return this
         }
       }

16-18: Single global cleanupCallback shared across all conversations.

setCleanupCallback stores one callback for the entire bridge. If multiple conversations stream concurrently, cleanup(convA) will invoke the same callback that was perhaps intended for convB. If multi-conversation streaming is a future possibility, consider making the callback per-conversation (e.g., stored in the StreamController).

packages/cozy-search/src/components/adapters/CozyRealtimeChatAdapter.ts (1)

90-95: Abort is only checked between chunks — not while waiting.

If the stream is blocked waiting for the next chunk in next(), the abortSignal.aborted check at line 91 won't run until a chunk arrives. For long pauses this means the UI appears unresponsive to cancellation. Registering an abort event listener that calls streamBridge.cleanup(conversationId) would make cancellation immediate.

♻️ Sketch
+    const onAbort = () => {
+      streamBridge.cleanup(conversationId)
+    }
+    abortSignal?.addEventListener('abort', onAbort, { once: true })
+
     try {
       await client.stackClient.fetchJSON(...)

       // ... for-await loop (remove the inner abortSignal check) ...

     } catch (error) {
       // ...
+    } finally {
+      abortSignal?.removeEventListener('abort', onAbort)
     }
packages/cozy-search/src/components/Views/AssistantView.jsx (1)

14-57: Extract shared dialog rendering logic.

The dialogs section (lines 33–54) is identically duplicated in AssistantDialog.jsx (lines 74–95). Consider extracting into a shared component like AssistantDialogsOverlay to reduce duplication, allowing both views to render the three conditional dialogs without repeating the same markup and logic.

packages/cozy-search/src/components/Conversations/ConversationListItem.jsx (2)

79-83: Inconsistent optional chaining on messages.length.

Line 52 uses conversation.messages?.length (with optional chaining) while line 81 uses conversation.messages.length (without). Although the outer ?.[] short-circuits safely when messages is nullish, the inconsistency is confusing and fragile if refactored later.

Proposed fix
             <Typography className="u-db u-ellipsis u-mb-half u-fz-xsmall">
               {
-                conversation.messages?.[conversation.messages.length - 1]
+                conversation.messages?.[conversation.messages?.length - 1]
                   ?.content
               }
             </Typography>

35-35: makeActions is recomputed on every render.

makeActions([share, rename, remove], { t }) creates a new actions array each render. Consider memoizing with useMemo keyed on t to avoid unnecessary work and stable references for ActionsMenu.

Proposed fix
+import React, { useState, useRef, useMemo } from 'react'
...
-  const actions = makeActions([share, rename, remove], { t })
+  const actions = useMemo(() => makeActions([share, rename, remove], { t }), [t])
packages/cozy-search/src/components/Conversations/ConversationListItemWider.jsx (1)

22-38: Significant code duplication with ConversationListItem.jsx.

Both components share identical state management, toggleMenu logic, action creation, imports, and i18n setup. Consider extracting a shared hook (e.g., useConversationItemMenu) to DRY up the common logic.

// e.g., useConversationItemMenu.js
export const useConversationItemMenu = () => {
  const { t, lang } = useI18n()
  const [isMenuOpen, setIsMenuOpen] = useState(false)
  const anchorRef = useRef(null)
  const toggleMenu = e => { e?.stopPropagation(); setIsMenuOpen(!isMenuOpen) }
  const actions = useMemo(() => makeActions([share, rename, remove], { t }), [t])
  return { t, lang, isMenuOpen, anchorRef, toggleMenu, actions }
}
packages/cozy-search/src/components/Conversations/ConversationBar.jsx (2)

71-94: IconButton has no onClick — clicks on its padding area are inert.

The onClick handlers (onCancel on line 79, handleSend on line 91) are bound to the inner Button (rendered as <div>). The outer IconButton has no onClick, so clicking its padding (outside the inner div) does nothing. Consider moving onClick to the IconButton or ensuring the inner div fills the entire clickable area.


29-33: Pass the ref object rather than ref.current, or attach the listener in useEffect for clarity.

useEventListener(inputRef.current, 'input', ...) captures undefined during the initial render since the ref hasn't been assigned yet. The listener only gets attached on a subsequent re-render when inputRef.current is populated. While this typically works because the component re-renders on value changes, it's fragile and relies on external re-renders for correctness.

Better patterns:

  • Pass the ref object itself and let useEventListener handle dereferencing internally, or
  • Move the listener setup into a useEffect that depends on the input element being available

Comment thread packages/cozy-search/package.json
Comment thread packages/cozy-search/src/components/Search/helpers.js Outdated
@lethemanh lethemanh force-pushed the new-assistant-ui branch 3 times, most recently from cf09f50 to b26247b Compare February 11, 2026 08:02
@zatteo
Copy link
Copy Markdown
Member

zatteo commented Feb 12, 2026

Last translation issue :'(

Screenshot 2026-02-12 at 17 09 51

@lethemanh
Copy link
Copy Markdown
Contributor Author

Some translations have been lost

Screenshot 2026-02-12 at 06 47 12 Screenshot 2026-02-12 at 06 47 25

I've added all the missing keys

</div>

<div className="u-ov-auto u-flex-auto">
{groupedConversations.today?.length > 0 && (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get why we group conversations by today vs older ?

msg.content?.toLowerCase().includes(lowerQuery)
)
)
}, [conversations, query])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, the actual search relies on the .includes filter. But this is quite sub-optimal, because:

  • it searches in all conversations, with no index. If we have plenty of content, this will be quite slow
  • I don't think we have all conversations loaded in memory, as this is currently paginated at 50 conversations
  • it will only work if the query is a subset of a conversation, but this will miss plenty of case, like if I have a conversation with "history of football" and I search "history football", it will fails.

@Crash-- We have fuse search for contacts, and I think we wanted to have similar search for admin panel ? Can we reuse it?

const { data: conversations } = useQuery(
conversationsQuery.definition,
conversationsQuery.options
)
Copy link
Copy Markdown
Contributor

@paultranvan paultranvan Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we load the 50 last conversations, which is ok, but what if I want more? We need to have some pagination to load more conversations
cc @zatteo

@lethemanh
Copy link
Copy Markdown
Contributor Author

@zatteo @paultranvan we will merge this PR and all the issues from the feedbacks will be resolved in another PR

@rezk2ll
Copy link
Copy Markdown
Member

rezk2ll commented Feb 26, 2026

solid pr, can be improved a bit

stuff we can fix

  1. Hardcoded English string in AssistantSelection.jsx:104<Typography variant="body1">Create Assistant</Typography> is not using the t() function. Every other string in the component is translated.

  2. Copy-paste error in all 3 action TODOsrename.jsx:37, share.jsx:37, and delete.jsx:37 all say // TO DO: Add action to remove. The rename and share ones should say "rename" and "share" respectively.

  3. getNameOfConversation is fragile (helpers.js:91-92) — Uses messages[length - 2] assuming the second-to-last message is always from the user. If a conversation has 0 or 1 messages, or the backend ever reorders, this returns undefined silently. Consider finding the last message with role === 'user' instead.

  4. cancelledMessageIdsRef grows unbounded (CozyAssistantRuntimeProvider.tsx:120) — Message IDs are added to the Set on cancellation but never removed. Over a long session with many regenerations, this leaks memory. Consider clearing it when the conversation changes or capping its size.

  5. selectedTwakeKnowledge is never sent to the backend — The full knowledge selection UI (drive/mail/chat panels) is wired up in state, but createCozyRealtimeChatAdapter never includes the selected sources in the POST body to /ai/chat/conversations/{conversationId}. The UI is decorative right now — is this intentional / planned for a follow-up?

some concerns

  1. Three stub actions shipped without implementationdelete.jsx, rename.jsx, and share.jsx all have empty action: () => {} bodies behind a feature flag (cozy.conversation-actions.enabled). If this is intentional scaffolding, that's fine, but it should be called out in the PR description.

  2. Duplicated dialog rendering — Both AssistantDialog and AssistantView render the exact same CreateAssistantDialog / EditAssistantDialog / DeleteAssistantDialog triplet. Extract this into a shared component to avoid drift.

  3. TwakeKnowledgePanel has 4 identical switch statements (lines 59-127) — getTitle(), getDescription(), getIcon(), and renderContent() all switch on openedKnowledgePanel. A config map would be cleaner:

    const PANEL_CONFIG = {
      drive: { title: 'title_drive', desc: 'desc_drive', icon: TDrive, Component: DriveKnowledge },
      mail:  { title: 'title_mail',  desc: 'desc_mail',  icon: TMail,  Component: MailKnowledge },
      chat:  { title: 'title_chat',  desc: 'desc_chat',  icon: TChat,  Component: ChatKnowledge },
    }
  4. URL manipulation in useConversation is brittle — Splitting on / and using findIndex('assistant') to reconstruct the path will break if routes are restructured. Consider using a route-relative approach or a centralized route builder.

  5. Client-side conversation search won't scaleSearchConversation fetches up to 50 conversations via CouchDB and filters them in-memory with Array.filter(). As conversation count grows, this needs server-side search or at a minimum pagination.

deps

  1. Unused direct dependenciesclass-variance-authority, clsx, and lucide-react are added to dependencies in package.json but are never imported anywhere in the source code. If they're transitive deps required by @assistant-ui/react at runtime, document why. Otherwise remove them.

  2. React >=18.0.0 is a breaking change — The peer dependency floor moved from >=16.12.0 to >=18.0.0. Any consumer still on React 17 will break. This should be called out prominently — is this a semver-major bump for cozy-search?

Missing

  1. No tests — ~4,000 lines of new code with zero test coverage. At minimum, StreamBridge, sanitizeChatContent, formatConversationDate, groupConversationsByDate, and getNameOfConversation are pure logic that can be unit-tested trivially.

  2. No error boundary around CozyAssistantRuntimeProvider — A WebSocket disconnect or malformed realtime event will crash the entire assistant UI. Consider wrapping with a React error boundary that shows a retry prompt.

Nits

  • AssistantAvatar returns bare undefined on if (!assistant) return (line 12) — prefer return null for React components.
  • PrettyScrollbar sets scrollbar-width: none by default and only shows on hover — this hides scrollability and may cause accessibility issues on non-mouse devices.

Copy link
Copy Markdown
Member

@rezk2ll rezk2ll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check my comment above

@zatteo
Copy link
Copy Markdown
Member

zatteo commented Feb 26, 2026

Thanks for the review @rezk2ll.

We wanted to merge this large PR and fix some already identified issues in future PR to avoid a never ending PR which has already 100+ comments.

Part of this PR is only decorative UI as you saw. It is intentional. @lethemanh you can add a word about it in PR description for future reader.

That said, my two cent:

  • could you double check 11. @lethemanh ? I don't want to add unused packages.
  • about 12. it is intentional. All our apps are or can be upgraded to React 18 except Notes which will be sunset in the next month.
  • otherwise I would suggest that @lethemanh fix your suggestions in next PR except maybe 1. or 2. that could be fixed here?

What do you think guys?

@lethemanh
Copy link
Copy Markdown
Contributor Author

lethemanh commented Feb 26, 2026

@rezk2ll thank for your comments, I have some feedback as below:

1. Hardcoded English string in AssistantSelection.jsx:104<Typography variant="body1">Create Assistant</Typography> is not using the t() function. Every other string in the component is translated.
4. cancelledMessageIdsRef grows unbounded (CozyAssistantRuntimeProvider.tsx:120) — Message IDs are added to the Set on cancellation but never removed. Over a long session with many regenerations, this leaks memory. Consider clearing it when the conversation changes or capping its size.
8. TwakeKnowledgePanel has 4 identical switch statements (lines 59-127) — getTitle(), getDescription(), getIcon(), and renderContent() all switch on openedKnowledgePanel. A config map would be cleaner:
9. URL manipulation in useConversation is brittle — Splitting on / and using findIndex('assistant') to reconstruct the path will break if routes are restructured. Consider using a route-relative approach or a centralized route builder.
13. No tests — ~4,000 lines of new code with zero test coverage. At minimum, StreamBridge, sanitizeChatContent, formatConversationDate, groupConversationsByDate, and getNameOfConversation are pure logic that can be unit-tested trivially.
14. No error boundary around CozyAssistantRuntimeProvider — A WebSocket disconnect or malformed realtime event will crash the entire assistant UI. Consider wrapping with a React error boundary that shows a retry prompt.

These will be fixed in the next PR.

2. Copy-paste error in all 3 action TODOsrename.jsx:37, share.jsx:37, and delete.jsx:37 all say // TO DO: Add action to remove. The rename and share ones should say "rename" and "share" respectively.
6. Three stub actions shipped without implementationdelete.jsx, rename.jsx, and share.jsx all have empty action: () => {} bodies behind a feature flag (cozy.conversation-actions.enabled). If this is intentional scaffolding, that's fine, but it should be called out in the PR description.

These 3 actions are not ready in the backend, so I put them under the flag cozy.conversation-actions.enabled.

3. getNameOfConversation is fragile (helpers.js:91-92) — Uses messages[length - 2] assuming the second-to-last message is always from the user. If a conversation has 0 or 1 messages, or the backend ever reorders, this returns undefined silently. Consider finding the last message with role === 'user' instead.

Currently, we don't have a specific spec for conversation name so it's temporary for now.

5. selectedTwakeKnowledge is never sent to the backend — The full knowledge selection UI (drive/mail/chat panels) is wired up in state, but createCozyRealtimeChatAdapter never includes the selected sources in the POST body to /ai/chat/conversations/{conversationId}. The UI is decorative right now — is this intentional / planned for a follow-up?

Currently, the backend does not support chat with the selected assistant, but in the next PR, each conversation will be stored with the selected assistant.

7. Duplicated dialog rendering — Both AssistantDialog and AssistantView render the exact same CreateAssistantDialog / EditAssistantDialog / DeleteAssistantDialog triplet. Extract this into a shared component to avoid drift.

For AssistantDialog and AssistantView they are 2 different UI of assistant which can be triggered by using flag cozy.top-bar-in-assistant.enabled. The AssistantView is used when enabled this flag. In the future, the AssistantDialog will be removed if we agree to display topbar in assistant.

10. Client-side conversation search won't scaleSearchConversation fetches up to 50 conversations via CouchDB and filters them in-memory with Array.filter(). As conversation count grows, this needs server-side search or at a minimum pagination.

Currently, the search action is not available, it's UI only.

11. Unused direct dependenciesclass-variance-authority, clsx, and lucide-react are added to dependencies in package.json but are never imported anywhere in the source code. If they're transitive deps required by @assistant-ui/react at runtime, document why. Otherwise remove them.
12. React >=18.0.0 is a breaking change — The peer dependency floor moved from >=16.12.0 to >=18.0.0. Any consumer still on React 17 will break. This should be called out prominently — is this a semver-major bump for cozy-search?

As @zatteo 's comment.

@lethemanh lethemanh requested a review from rezk2ll February 26, 2026 09:05
@rezk2ll
Copy link
Copy Markdown
Member

rezk2ll commented Feb 26, 2026

@rezk2ll thank for your comments, I have some feedback as below:

As @zatteo 's comment.

ok as long as you promise to fix them in a new PR 😆

@lethemanh lethemanh merged commit 000b819 into master Mar 2, 2026
3 checks passed
@lethemanh lethemanh deleted the new-assistant-ui branch March 2, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants